new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -0,0 +1,310 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+import java.io.IOException;
+
+/**
+ * An abstract base class suitable for most {@link Renderer} implementations.
+ */
+public abstract class BaseRenderer implements Renderer, RendererCapabilities {
+
+ private final int trackType;
+
+ private RendererConfiguration configuration;
+ private int index;
+ private int state;
+ private SampleStream stream;
+ private long streamOffsetUs;
+ private boolean readEndOfStream;
+ private boolean streamIsFinal;
+
+ /**
+ * @param trackType The track type that the renderer handles. One of the {@link C}
+ * {@code TRACK_TYPE_*} constants.
+ */
+ public BaseRenderer(int trackType) {
+ this.trackType = trackType;
+ readEndOfStream = true;
+ }
+
+ @Override
+ public final int getTrackType() {
+ return trackType;
+ }
+
+ @Override
+ public final RendererCapabilities getCapabilities() {
+ return this;
+ }
+
+ @Override
+ public final void setIndex(int index) {
+ this.index = index;
+ }
+
+ @Override
+ public MediaClock getMediaClock() {
+ return null;
+ }
+
+ @Override
+ public final int getState() {
+ return state;
+ }
+
+ @Override
+ public final void enable(RendererConfiguration configuration, Format[] formats,
+ SampleStream stream, long positionUs, boolean joining, long offsetUs)
+ throws ExoPlaybackException {
+ Assertions.checkState(state == STATE_DISABLED);
+ this.configuration = configuration;
+ state = STATE_ENABLED;
+ onEnabled(joining);
+ replaceStream(formats, stream, offsetUs);
+ onPositionReset(positionUs, joining);
+ }
+
+ @Override
+ public final void start() throws ExoPlaybackException {
+ Assertions.checkState(state == STATE_ENABLED);
+ state = STATE_STARTED;
+ onStarted();
+ }
+
+ @Override
+ public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
+ throws ExoPlaybackException {
+ Assertions.checkState(!streamIsFinal);
+ this.stream = stream;
+ readEndOfStream = false;
+ streamOffsetUs = offsetUs;
+ onStreamChanged(formats);
+ }
+
+ @Override
+ public final SampleStream getStream() {
+ return stream;
+ }
+
+ @Override
+ public final boolean hasReadStreamToEnd() {
+ return readEndOfStream;
+ }
+
+ @Override
+ public final void setCurrentStreamFinal() {
+ streamIsFinal = true;
+ }
+
+ @Override
+ public final boolean isCurrentStreamFinal() {
+ return streamIsFinal;
+ }
+
+ @Override
+ public final void maybeThrowStreamError() throws IOException {
+ stream.maybeThrowError();
+ }
+
+ @Override
+ public final void resetPosition(long positionUs) throws ExoPlaybackException {
+ streamIsFinal = false;
+ readEndOfStream = false;
+ onPositionReset(positionUs, false);
+ }
+
+ @Override
+ public final void stop() throws ExoPlaybackException {
+ Assertions.checkState(state == STATE_STARTED);
+ state = STATE_ENABLED;
+ onStopped();
+ }
+
+ @Override
+ public final void disable() {
+ Assertions.checkState(state == STATE_ENABLED);
+ state = STATE_DISABLED;
+ onDisabled();
+ stream = null;
+ streamIsFinal = false;
+ }
+
+ // RendererCapabilities implementation.
+
+ @Override
+ public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+ return ADAPTIVE_NOT_SUPPORTED;
+ }
+
+ // ExoPlayerComponent implementation.
+
+ @Override
+ public void handleMessage(int what, Object object) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ // Methods to be overridden by subclasses.
+
+ /**
+ * Called when the renderer is enabled.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param joining Whether this renderer is being enabled to join an ongoing playback.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the renderer's stream has changed. This occurs when the renderer is enabled after
+ * {@link #onEnabled(boolean)} has been called, and also when the stream has been replaced whilst
+ * the renderer is enabled or started.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param formats The enabled formats.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the position is reset. This occurs when the renderer is enabled after
+ * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity
+ * is encountered.
+ * <p>
+ * After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples
+ * starting from a key frame.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param positionUs The new playback position in microseconds.
+ * @param joining Whether this renderer is being enabled to join an ongoing playback.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the renderer is started.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onStarted() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the renderer is stopped.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onStopped() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called when the renderer is disabled.
+ * <p>
+ * The default implementation is a no-op.
+ */
+ protected void onDisabled() {
+ // Do nothing.
+ }
+
+ // Methods to be called by subclasses.
+
+ /**
+ * Returns the configuration set when the renderer was most recently enabled.
+ */
+ protected final RendererConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * Returns the index of the renderer within the player.
+ */
+ protected final int getIndex() {
+ return index;
+ }
+
+ /**
+ * Reads from the enabled upstream source. If the upstream source has been read to the end then
+ * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
+ * called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ int result = stream.readData(formatHolder, buffer, formatRequired);
+ if (result == C.RESULT_BUFFER_READ) {
+ if (buffer.isEndOfStream()) {
+ readEndOfStream = true;
+ return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
+ }
+ buffer.timeUs += streamOffsetUs;
+ } else if (result == C.RESULT_FORMAT_READ) {
+ Format format = formatHolder.format;
+ if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs);
+ formatHolder.format = format;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Attempts to skip to the keyframe before the specified position, or to the end of the stream if
+ * {@code positionUs} is beyond it.
+ *
+ * @param positionUs The position in microseconds.
+ */
+ protected void skipSource(long positionUs) {
+ stream.skipData(positionUs - streamOffsetUs);
+ }
+
+ /**
+ * Returns whether the upstream source is ready.
+ *
+ * @return Whether the source is ready.
+ */
+ protected final boolean isSourceReady() {
+ return readEndOfStream ? streamIsFinal : stream.isReady();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/C.java
@@ -0,0 +1,664 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.support.annotation.IntDef;
+import android.view.Surface;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.UUID;
+
+/**
+ * Defines constants used by the library.
+ */
+public final class C {
+
+ private C() {}
+
+ /**
+ * Special constant representing a time corresponding to the end of a source. Suitable for use in
+ * any time base.
+ */
+ public static final long TIME_END_OF_SOURCE = Long.MIN_VALUE;
+
+ /**
+ * Special constant representing an unset or unknown time or duration. Suitable for use in any
+ * time base.
+ */
+ public static final long TIME_UNSET = Long.MIN_VALUE + 1;
+
+ /**
+ * Represents an unset or unknown index.
+ */
+ public static final int INDEX_UNSET = -1;
+
+ /**
+ * Represents an unset or unknown position.
+ */
+ public static final int POSITION_UNSET = -1;
+
+ /**
+ * Represents an unset or unknown length.
+ */
+ public static final int LENGTH_UNSET = -1;
+
+ /**
+ * The number of microseconds in one second.
+ */
+ public static final long MICROS_PER_SECOND = 1000000L;
+
+ /**
+ * The number of nanoseconds in one second.
+ */
+ public static final long NANOS_PER_SECOND = 1000000000L;
+
+ /**
+ * The name of the UTF-8 charset.
+ */
+ public static final String UTF8_NAME = "UTF-8";
+
+ /**
+ * The name of the UTF-16 charset.
+ */
+ public static final String UTF16_NAME = "UTF-16";
+
+ /**
+ * * The name of the serif font family.
+ */
+ public static final String SERIF_NAME = "serif";
+
+ /**
+ * * The name of the sans-serif font family.
+ */
+ public static final String SANS_SERIF_NAME = "sans-serif";
+
+ /**
+ * Crypto modes for a codec.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
+ public @interface CryptoMode {}
+ /**
+ * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
+ /**
+ * @see MediaCodec#CRYPTO_MODE_AES_CTR
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
+ /**
+ * @see MediaCodec#CRYPTO_MODE_AES_CBC
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
+
+ /**
+ * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
+ * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+ /**
+ * Represents an audio encoding, or an invalid or unset value.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
+ ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS,
+ ENCODING_DTS_HD})
+ public @interface Encoding {}
+
+ /**
+ * Represents a PCM audio encoding, or an invalid or unset value.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
+ ENCODING_PCM_24BIT, ENCODING_PCM_32BIT})
+ public @interface PcmEncoding {}
+ /**
+ * @see AudioFormat#ENCODING_INVALID
+ */
+ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
+ /**
+ * @see AudioFormat#ENCODING_PCM_8BIT
+ */
+ public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
+ /**
+ * @see AudioFormat#ENCODING_PCM_16BIT
+ */
+ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
+ /**
+ * PCM encoding with 24 bits per sample.
+ */
+ public static final int ENCODING_PCM_24BIT = 0x80000000;
+ /**
+ * PCM encoding with 32 bits per sample.
+ */
+ public static final int ENCODING_PCM_32BIT = 0x40000000;
+ /**
+ * @see AudioFormat#ENCODING_AC3
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
+ /**
+ * @see AudioFormat#ENCODING_E_AC3
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+ /**
+ * @see AudioFormat#ENCODING_DTS
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
+ /**
+ * @see AudioFormat#ENCODING_DTS_HD
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
+
+ /**
+ * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
+ */
+ @SuppressWarnings({"InlinedApi", "deprecation"})
+ public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
+ ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
+
+ /**
+ * Stream types for an {@link android.media.AudioTrack}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
+ STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
+ public @interface StreamType {}
+ /**
+ * @see AudioManager#STREAM_ALARM
+ */
+ public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
+ /**
+ * @see AudioManager#STREAM_MUSIC
+ */
+ public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
+ /**
+ * @see AudioManager#STREAM_NOTIFICATION
+ */
+ public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
+ /**
+ * @see AudioManager#STREAM_RING
+ */
+ public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
+ /**
+ * @see AudioManager#STREAM_SYSTEM
+ */
+ public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
+ /**
+ * @see AudioManager#STREAM_VOICE_CALL
+ */
+ public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
+ /**
+ * The default stream type used by audio renderers.
+ */
+ public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
+
+ /**
+ * Flags which can apply to a buffer containing a media sample.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM,
+ BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY})
+ public @interface BufferFlags {}
+ /**
+ * Indicates that a buffer holds a synchronization sample.
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
+ /**
+ * Flag for empty buffers that signal that the end of the stream was reached.
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ /**
+ * Indicates that a buffer is (at least partially) encrypted.
+ */
+ public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
+ /**
+ * Indicates that a buffer should be decoded but not rendered.
+ */
+ public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
+
+ /**
+ * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
+ public @interface VideoScalingMode {}
+ /**
+ * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
+ /**
+ * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
+ /**
+ * A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s.
+ */
+ public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;
+
+ /**
+ * Track selection flags.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED,
+ SELECTION_FLAG_AUTOSELECT})
+ public @interface SelectionFlags {}
+ /**
+ * Indicates that the track should be selected if user preferences do not state otherwise.
+ */
+ public static final int SELECTION_FLAG_DEFAULT = 1;
+ /**
+ * Indicates that the track must be displayed. Only applies to text tracks.
+ */
+ public static final int SELECTION_FLAG_FORCED = 2;
+ /**
+ * Indicates that the player may choose to play the track in absence of an explicit user
+ * preference.
+ */
+ public static final int SELECTION_FLAG_AUTOSELECT = 4;
+
+ /**
+ * Represents a streaming or other media type.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})
+ public @interface ContentType {}
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for DASH manifests.
+ */
+ public static final int TYPE_DASH = 0;
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests.
+ */
+ public static final int TYPE_SS = 1;
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for HLS manifests.
+ */
+ public static final int TYPE_HLS = 2;
+ /**
+ * Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
+ * Smooth Streaming manifests.
+ */
+ public static final int TYPE_OTHER = 3;
+
+ /**
+ * A return value for methods where the end of an input was encountered.
+ */
+ public static final int RESULT_END_OF_INPUT = -1;
+ /**
+ * A return value for methods where the length of parsed data exceeds the maximum length allowed.
+ */
+ public static final int RESULT_MAX_LENGTH_EXCEEDED = -2;
+ /**
+ * A return value for methods where nothing was read.
+ */
+ public static final int RESULT_NOTHING_READ = -3;
+ /**
+ * A return value for methods where a buffer was read.
+ */
+ public static final int RESULT_BUFFER_READ = -4;
+ /**
+ * A return value for methods where a format was read.
+ */
+ public static final int RESULT_FORMAT_READ = -5;
+
+ /**
+ * A data type constant for data of unknown or unspecified type.
+ */
+ public static final int DATA_TYPE_UNKNOWN = 0;
+ /**
+ * A data type constant for media, typically containing media samples.
+ */
+ public static final int DATA_TYPE_MEDIA = 1;
+ /**
+ * A data type constant for media, typically containing only initialization data.
+ */
+ public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;
+ /**
+ * A data type constant for drm or encryption data.
+ */
+ public static final int DATA_TYPE_DRM = 3;
+ /**
+ * A data type constant for a manifest file.
+ */
+ public static final int DATA_TYPE_MANIFEST = 4;
+ /**
+ * A data type constant for time synchronization data.
+ */
+ public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
+ /**
+ * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
+ * equal to this value.
+ */
+ public static final int DATA_TYPE_CUSTOM_BASE = 10000;
+
+ /**
+ * A type constant for tracks of unknown type.
+ */
+ public static final int TRACK_TYPE_UNKNOWN = -1;
+ /**
+ * A type constant for tracks of some default type, where the type itself is unknown.
+ */
+ public static final int TRACK_TYPE_DEFAULT = 0;
+ /**
+ * A type constant for audio tracks.
+ */
+ public static final int TRACK_TYPE_AUDIO = 1;
+ /**
+ * A type constant for video tracks.
+ */
+ public static final int TRACK_TYPE_VIDEO = 2;
+ /**
+ * A type constant for text tracks.
+ */
+ public static final int TRACK_TYPE_TEXT = 3;
+ /**
+ * A type constant for metadata tracks.
+ */
+ public static final int TRACK_TYPE_METADATA = 4;
+ /**
+ * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or
+ * equal to this value.
+ */
+ public static final int TRACK_TYPE_CUSTOM_BASE = 10000;
+
+ /**
+ * A selection reason constant for selections whose reasons are unknown or unspecified.
+ */
+ public static final int SELECTION_REASON_UNKNOWN = 0;
+ /**
+ * A selection reason constant for an initial track selection.
+ */
+ public static final int SELECTION_REASON_INITIAL = 1;
+ /**
+ * A selection reason constant for an manual (i.e. user initiated) track selection.
+ */
+ public static final int SELECTION_REASON_MANUAL = 2;
+ /**
+ * A selection reason constant for an adaptive track selection.
+ */
+ public static final int SELECTION_REASON_ADAPTIVE = 3;
+ /**
+ * A selection reason constant for a trick play track selection.
+ */
+ public static final int SELECTION_REASON_TRICK_PLAY = 4;
+ /**
+ * Applications or extensions may define custom {@code SELECTION_REASON_*} constants greater than
+ * or equal to this value.
+ */
+ public static final int SELECTION_REASON_CUSTOM_BASE = 10000;
+
+ /**
+ * A default size in bytes for an individual allocation that forms part of a larger buffer.
+ */
+ public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
+
+ /**
+ * A default size in bytes for a video buffer.
+ */
+ public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+ /**
+ * A default size in bytes for an audio buffer.
+ */
+ public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+ /**
+ * A default size in bytes for a text buffer.
+ */
+ public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+ /**
+ * A default size in bytes for a metadata buffer.
+ */
+ public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+ /**
+ * A default size in bytes for a muxed buffer (e.g. containing video, audio and text).
+ */
+ public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE
+ + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE;
+
+ /**
+ * The Nil UUID as defined by
+ * <a href="https://tools.ietf.org/html/rfc4122#section-4.1.7">RFC4122</a>.
+ */
+ public static final UUID UUID_NIL = new UUID(0L, 0L);
+
+ /**
+ * UUID for the ClearKey DRM scheme.
+ * <p>
+ * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
+ */
+ public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
+
+ /**
+ * UUID for the Widevine DRM scheme.
+ * <p>
+ * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
+ */
+ public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
+
+ /**
+ * UUID for the PlayReady DRM scheme.
+ * <p>
+ * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not
+ * provide PlayReady support.
+ */
+ public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L);
+
+ /**
+ * The type of a message that can be passed to a video {@link Renderer} via
+ * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+ * should be the target {@link Surface}, or null.
+ */
+ public static final int MSG_SET_SURFACE = 1;
+
+ /**
+ * A type of a message that can be passed to an audio {@link Renderer} via
+ * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+ * should be a {@link Float} with 0 being silence and 1 being unity gain.
+ */
+ public static final int MSG_SET_VOLUME = 2;
+
+ /**
+ * A type of a message that can be passed to an audio {@link Renderer} via
+ * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+ * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
+ * type of the underlying {@link android.media.AudioTrack}. See also
+ * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
+ * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
+ * <p>
+ * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
+ * introduce a brief gap in audio output. Note also that tracks in the same audio session must
+ * share the same routing, so a new audio session id will be generated.
+ */
+ public static final int MSG_SET_STREAM_TYPE = 3;
+
+ /**
+ * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
+ * via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message
+ * object should be one of the integer scaling modes in {@link C.VideoScalingMode}.
+ * <p>
+ * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
+ * owned by a {@link android.view.SurfaceView}.
+ */
+ public static final int MSG_SET_SCALING_MODE = 4;
+
+ /**
+ * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
+ * this value.
+ */
+ public static final int MSG_CUSTOM_BASE = 10000;
+
+ /**
+ * The stereo mode for 360/3D/VR videos.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ Format.NO_VALUE,
+ STEREO_MODE_MONO,
+ STEREO_MODE_TOP_BOTTOM,
+ STEREO_MODE_LEFT_RIGHT,
+ STEREO_MODE_STEREO_MESH
+ })
+ public @interface StereoMode {}
+ /**
+ * Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_MONO = 0;
+ /**
+ * Indicates Top-Bottom stereo layout, used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_TOP_BOTTOM = 1;
+ /**
+ * Indicates Left-Right stereo layout, used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_LEFT_RIGHT = 2;
+ /**
+ * Indicates a stereo layout where the left and right eyes have separate meshes,
+ * used with 360/3D/VR videos.
+ */
+ public static final int STEREO_MODE_STEREO_MESH = 3;
+
+ /**
+ * Video colorspaces.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})
+ public @interface ColorSpace {}
+ /**
+ * @see MediaFormat#COLOR_STANDARD_BT709
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
+ /**
+ * @see MediaFormat#COLOR_STANDARD_BT601_PAL
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
+ /**
+ * @see MediaFormat#COLOR_STANDARD_BT2020
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
+
+ /**
+ * Video color transfer characteristics.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG})
+ public @interface ColorTransfer {}
+ /**
+ * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;
+ /**
+ * @see MediaFormat#COLOR_TRANSFER_ST2084
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;
+ /**
+ * @see MediaFormat#COLOR_TRANSFER_HLG
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
+
+ /**
+ * Video color range.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})
+ public @interface ColorRange {}
+ /**
+ * @see MediaFormat#COLOR_RANGE_LIMITED
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
+ /**
+ * @see MediaFormat#COLOR_RANGE_FULL
+ */
+ @SuppressWarnings("InlinedApi")
+ public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
+
+ /**
+ * Priority for media playback.
+ *
+ * <p>Larger values indicate higher priorities.
+ */
+ public static final int PRIORITY_PLAYBACK = 0;
+
+ /**
+ * Priority for media downloading.
+ *
+ * <p>Larger values indicate higher priorities.
+ */
+ public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000;
+
+ /**
+ * Converts a time in microseconds to the corresponding time in milliseconds, preserving
+ * {@link #TIME_UNSET} values.
+ *
+ * @param timeUs The time in microseconds.
+ * @return The corresponding time in milliseconds.
+ */
+ public static long usToMs(long timeUs) {
+ return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000);
+ }
+
+ /**
+ * Converts a time in milliseconds to the corresponding time in microseconds, preserving
+ * {@link #TIME_UNSET} values.
+ *
+ * @param timeMs The time in milliseconds.
+ * @return The corresponding time in microseconds.
+ */
+ public static long msToUs(long timeMs) {
+ return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
+ }
+
+ /**
+ * Returns a newly generated {@link android.media.AudioTrack} session identifier.
+ */
+ @TargetApi(21)
+ public static int generateAudioSessionIdV21(Context context) {
+ return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
+ .generateAudioSessionId();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -0,0 +1,205 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * The default {@link LoadControl} implementation.
+ */
+public final class DefaultLoadControl implements LoadControl {
+
+ /**
+ * The default minimum duration of media that the player will attempt to ensure is buffered at all
+ * times, in milliseconds.
+ */
+ public static final int DEFAULT_MIN_BUFFER_MS = 15000;
+
+ /**
+ * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+ */
+ public static final int DEFAULT_MAX_BUFFER_MS = 30000;
+
+ /**
+ * The default duration of media that must be buffered for playback to start or resume following a
+ * user action such as a seek, in milliseconds.
+ */
+ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500;
+
+ /**
+ * The default duration of media that must be buffered for playback to resume after a rebuffer,
+ * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
+ * action.
+ */
+ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
+
+ private static final int ABOVE_HIGH_WATERMARK = 0;
+ private static final int BETWEEN_WATERMARKS = 1;
+ private static final int BELOW_LOW_WATERMARK = 2;
+
+ private final DefaultAllocator allocator;
+
+ private final long minBufferUs;
+ private final long maxBufferUs;
+ private final long bufferForPlaybackUs;
+ private final long bufferForPlaybackAfterRebufferUs;
+ private final PriorityTaskManager priorityTaskManager;
+
+ private int targetBufferSize;
+ private boolean isBuffering;
+
+ /**
+ * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+ */
+ public DefaultLoadControl() {
+ this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
+ }
+
+ /**
+ * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+ *
+ * @param allocator The {@link DefaultAllocator} used by the loader.
+ */
+ public DefaultLoadControl(DefaultAllocator allocator) {
+ this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+ DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param allocator The {@link DefaultAllocator} used by the loader.
+ * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+ * buffered at all times, in milliseconds.
+ * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
+ * milliseconds.
+ * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+ * resume following a user action such as a seek, in milliseconds.
+ * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+ * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+ * buffer depletion rather than a user action.
+ */
+ public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
+ long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
+ this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
+ null);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param allocator The {@link DefaultAllocator} used by the loader.
+ * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+ * buffered at all times, in milliseconds.
+ * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
+ * milliseconds.
+ * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+ * resume following a user action such as a seek, in milliseconds.
+ * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+ * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+ * buffer depletion rather than a user action.
+ * @param priorityTaskManager If not null, registers itself as a task with priority
+ * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
+ * periods.
+ */
+ public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
+ long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
+ PriorityTaskManager priorityTaskManager) {
+ this.allocator = allocator;
+ minBufferUs = minBufferMs * 1000L;
+ maxBufferUs = maxBufferMs * 1000L;
+ bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
+ bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
+ this.priorityTaskManager = priorityTaskManager;
+ }
+
+ @Override
+ public void onPrepared() {
+ reset(false);
+ }
+
+ @Override
+ public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+ TrackSelectionArray trackSelections) {
+ targetBufferSize = 0;
+ for (int i = 0; i < renderers.length; i++) {
+ if (trackSelections.get(i) != null) {
+ targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
+ }
+ }
+ allocator.setTargetBufferSize(targetBufferSize);
+ }
+
+ @Override
+ public void onStopped() {
+ reset(true);
+ }
+
+ @Override
+ public void onReleased() {
+ reset(true);
+ }
+
+ @Override
+ public Allocator getAllocator() {
+ return allocator;
+ }
+
+ @Override
+ public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
+ long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
+ return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs;
+ }
+
+ @Override
+ public boolean shouldContinueLoading(long bufferedDurationUs) {
+ int bufferTimeState = getBufferTimeState(bufferedDurationUs);
+ boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
+ boolean wasBuffering = isBuffering;
+ isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
+ || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
+ if (priorityTaskManager != null && isBuffering != wasBuffering) {
+ if (isBuffering) {
+ priorityTaskManager.add(C.PRIORITY_PLAYBACK);
+ } else {
+ priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
+ }
+ }
+ return isBuffering;
+ }
+
+ private int getBufferTimeState(long bufferedDurationUs) {
+ return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
+ : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
+ }
+
+ private void reset(boolean resetAllocator) {
+ targetBufferSize = 0;
+ if (priorityTaskManager != null && isBuffering) {
+ priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
+ }
+ isBuffering = false;
+ if (resetAllocator) {
+ allocator.reset();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -0,0 +1,327 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import com.google.android.exoplayer2.audio.AudioCapabilities;
+import com.google.android.exoplayer2.audio.AudioProcessor;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+
+/**
+ * Default {@link RenderersFactory} implementation.
+ */
+public class DefaultRenderersFactory implements RenderersFactory {
+
+ /**
+ * The default maximum duration for which a video renderer can attempt to seamlessly join an
+ * ongoing playback.
+ */
+ public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000;
+
+ /**
+ * Modes for using extension renderers.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON,
+ EXTENSION_RENDERER_MODE_PREFER})
+ public @interface ExtensionRendererMode {}
+ /**
+ * Do not allow use of extension renderers.
+ */
+ public static final int EXTENSION_RENDERER_MODE_OFF = 0;
+ /**
+ * Allow use of extension renderers. Extension renderers are indexed after core renderers of the
+ * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
+ * prefer to use a core renderer to an extension renderer in the case that both are able to play
+ * a given track.
+ */
+ public static final int EXTENSION_RENDERER_MODE_ON = 1;
+ /**
+ * Allow use of extension renderers. Extension renderers are indexed before core renderers of the
+ * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
+ * prefer to use an extension renderer to a core renderer in the case that both are able to play
+ * a given track.
+ */
+ public static final int EXTENSION_RENDERER_MODE_PREFER = 2;
+
+ private static final String TAG = "DefaultRenderersFactory";
+
+ protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
+
+ private final Context context;
+ private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+ private final @ExtensionRendererMode int extensionRendererMode;
+ private final long allowedVideoJoiningTimeMs;
+
+ /**
+ * @param context A {@link Context}.
+ */
+ public DefaultRenderersFactory(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * @param context A {@link Context}.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
+ * playbacks are not required.
+ */
+ public DefaultRenderersFactory(Context context,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF);
+ }
+
+ /**
+ * @param context A {@link Context}.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
+ * playbacks are not required..
+ * @param extensionRendererMode The extension renderer mode, which determines if and how
+ * available extension renderers are used. Note that extensions must be included in the
+ * application build for them to be considered available.
+ */
+ public DefaultRenderersFactory(Context context,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ @ExtensionRendererMode int extensionRendererMode) {
+ this(context, drmSessionManager, extensionRendererMode,
+ DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
+ }
+
+ /**
+ * @param context A {@link Context}.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
+ * playbacks are not required..
+ * @param extensionRendererMode The extension renderer mode, which determines if and how
+ * available extension renderers are used. Note that extensions must be included in the
+ * application build for them to be considered available.
+ * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt
+ * to seamlessly join an ongoing playback.
+ */
+ public DefaultRenderersFactory(Context context,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) {
+ this.context = context;
+ this.drmSessionManager = drmSessionManager;
+ this.extensionRendererMode = extensionRendererMode;
+ this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
+ }
+
+ @Override
+ public Renderer[] createRenderers(Handler eventHandler,
+ VideoRendererEventListener videoRendererEventListener,
+ AudioRendererEventListener audioRendererEventListener,
+ TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) {
+ ArrayList<Renderer> renderersList = new ArrayList<>();
+ buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
+ eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
+ buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(),
+ eventHandler, audioRendererEventListener, extensionRendererMode, renderersList);
+ buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
+ extensionRendererMode, renderersList);
+ buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
+ extensionRendererMode, renderersList);
+ buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
+ return renderersList.toArray(new Renderer[renderersList.size()]);
+ }
+
+ /**
+ * Builds video renderers for use by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
+ * will not be used for DRM protected playbacks.
+ * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video
+ * renderers can attempt to seamlessly join an ongoing playback.
+ * @param eventHandler A handler associated with the main thread's looper.
+ * @param eventListener An event listener.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildVideoRenderers(Context context,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, long allowedVideoJoiningTimeMs,
+ Handler eventHandler, VideoRendererEventListener eventListener,
+ @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
+ out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT,
+ allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener,
+ MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
+
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
+ return;
+ }
+ int extensionRendererIndex = out.size();
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+ extensionRendererIndex--;
+ }
+
+ try {
+ Class<?> clazz =
+ Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
+ Constructor<?> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class,
+ VideoRendererEventListener.class, int.class);
+ Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
+ eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
+ out.add(extensionRendererIndex++, renderer);
+ Log.i(TAG, "Loaded LibvpxVideoRenderer.");
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the extension.
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Builds audio renderers for use by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
+ * will not be used for DRM protected playbacks.
+ * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio
+ * buffers before output. May be empty.
+ * @param eventHandler A handler to use when invoking event listeners and outputs.
+ * @param eventListener An event listener.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildAudioRenderers(Context context,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ AudioProcessor[] audioProcessors, Handler eventHandler,
+ AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
+ ArrayList<Renderer> out) {
+ out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
+ eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors));
+
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
+ return;
+ }
+ int extensionRendererIndex = out.size();
+ if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+ extensionRendererIndex--;
+ }
+
+ try {
+ Class<?> clazz =
+ Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
+ Constructor<?> constructor = clazz.getConstructor(Handler.class,
+ AudioRendererEventListener.class, AudioProcessor[].class);
+ Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
+ audioProcessors);
+ out.add(extensionRendererIndex++, renderer);
+ Log.i(TAG, "Loaded LibopusAudioRenderer.");
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the extension.
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ Class<?> clazz =
+ Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
+ Constructor<?> constructor = clazz.getConstructor(Handler.class,
+ AudioRendererEventListener.class, AudioProcessor[].class);
+ Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
+ audioProcessors);
+ out.add(extensionRendererIndex++, renderer);
+ Log.i(TAG, "Loaded LibflacAudioRenderer.");
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the extension.
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ Class<?> clazz =
+ Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
+ Constructor<?> constructor = clazz.getConstructor(Handler.class,
+ AudioRendererEventListener.class, AudioProcessor[].class);
+ Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
+ audioProcessors);
+ out.add(extensionRendererIndex++, renderer);
+ Log.i(TAG, "Loaded FfmpegAudioRenderer.");
+ } catch (ClassNotFoundException e) {
+ // Expected if the app was built without the extension.
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Builds text renderers for use by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @param output An output for the renderers.
+ * @param outputLooper The looper associated with the thread on which the output should be
+ * called.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildTextRenderers(Context context, TextRenderer.Output output,
+ Looper outputLooper, @ExtensionRendererMode int extensionRendererMode,
+ ArrayList<Renderer> out) {
+ out.add(new TextRenderer(output, outputLooper));
+ }
+
+ /**
+ * Builds metadata renderers for use by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @param output An output for the renderers.
+ * @param outputLooper The looper associated with the thread on which the output should be
+ * called.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildMetadataRenderers(Context context, MetadataRenderer.Output output,
+ Looper outputLooper, @ExtensionRendererMode int extensionRendererMode,
+ ArrayList<Renderer> out) {
+ out.add(new MetadataRenderer(output, outputLooper));
+ }
+
+ /**
+ * Builds any miscellaneous renderers used by the player.
+ *
+ * @param context The {@link Context} associated with the player.
+ * @param eventHandler A handler to use when invoking event listeners and outputs.
+ * @param extensionRendererMode The extension renderer mode.
+ * @param out An array to which the built renderers should be appended.
+ */
+ protected void buildMiscellaneousRenderers(Context context, Handler eventHandler,
+ @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
+ // Do nothing.
+ }
+
+ /**
+ * Builds an array of {@link AudioProcessor}s that will process PCM audio before output.
+ */
+ protected AudioProcessor[] buildAudioProcessors() {
+ return new AudioProcessor[0];
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -0,0 +1,134 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Thrown when a non-recoverable playback failure occurs.
+ */
+public final class ExoPlaybackException extends Exception {
+
+ /**
+ * The type of source that produced the error.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
+ public @interface Type {}
+ /**
+ * The error occurred loading data from a {@link MediaSource}.
+ * <p>
+ * Call {@link #getSourceException()} to retrieve the underlying cause.
+ */
+ public static final int TYPE_SOURCE = 0;
+ /**
+ * The error occurred in a {@link Renderer}.
+ * <p>
+ * Call {@link #getRendererException()} to retrieve the underlying cause.
+ */
+ public static final int TYPE_RENDERER = 1;
+ /**
+ * The error was an unexpected {@link RuntimeException}.
+ * <p>
+ * Call {@link #getUnexpectedException()} to retrieve the underlying cause.
+ */
+ public static final int TYPE_UNEXPECTED = 2;
+
+ /**
+ * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
+ * {@link #TYPE_UNEXPECTED}.
+ */
+ @Type public final int type;
+
+ /**
+ * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
+ */
+ public final int rendererIndex;
+
+ /**
+ * Creates an instance of type {@link #TYPE_RENDERER}.
+ *
+ * @param cause The cause of the failure.
+ * @param rendererIndex The index of the renderer in which the failure occurred.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) {
+ return new ExoPlaybackException(TYPE_RENDERER, null, cause, rendererIndex);
+ }
+
+ /**
+ * Creates an instance of type {@link #TYPE_SOURCE}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ public static ExoPlaybackException createForSource(IOException cause) {
+ return new ExoPlaybackException(TYPE_SOURCE, null, cause, C.INDEX_UNSET);
+ }
+
+ /**
+ * Creates an instance of type {@link #TYPE_UNEXPECTED}.
+ *
+ * @param cause The cause of the failure.
+ * @return The created instance.
+ */
+ /* package */ static ExoPlaybackException createForUnexpected(RuntimeException cause) {
+ return new ExoPlaybackException(TYPE_UNEXPECTED, null, cause, C.INDEX_UNSET);
+ }
+
+ private ExoPlaybackException(@Type int type, String message, Throwable cause,
+ int rendererIndex) {
+ super(message, cause);
+ this.type = type;
+ this.rendererIndex = rendererIndex;
+ }
+
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_SOURCE}.
+ */
+ public IOException getSourceException() {
+ Assertions.checkState(type == TYPE_SOURCE);
+ return (IOException) getCause();
+ }
+
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_RENDERER}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_RENDERER}.
+ */
+ public Exception getRendererException() {
+ Assertions.checkState(type == TYPE_RENDERER);
+ return (Exception) getCause();
+ }
+
+ /**
+ * Retrieves the underlying error when {@link #type} is {@link #TYPE_UNEXPECTED}.
+ *
+ * @throws IllegalStateException If {@link #type} is not {@link #TYPE_UNEXPECTED}.
+ */
+ public RuntimeException getUnexpectedException() {
+ Assertions.checkState(type == TYPE_UNEXPECTED);
+ return (RuntimeException) getCause();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -0,0 +1,495 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+
+/**
+ * An extensible media player exposing traditional high-level media player functionality, such as
+ * the ability to buffer media, play, pause and seek. Instances can be obtained from
+ * {@link ExoPlayerFactory}.
+ *
+ * <h3>Player composition</h3>
+ * <p>ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the
+ * type of the media being played, how and where it is stored, and how it is rendered. Rather than
+ * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this
+ * work to components that are injected when a player is created or when it's prepared for playback.
+ * Components common to all ExoPlayer implementations are:
+ * <ul>
+ * <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from
+ * which the loaded media can be read. A MediaSource is injected via {@link #prepare} at the start
+ * of playback. The library modules provide default implementations for regular media files
+ * ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS
+ * (HlsMediaSource), implementations for merging ({@link MergingMediaSource}) and concatenating
+ * ({@link ConcatenatingMediaSource}) other MediaSources, and an implementation for loading single
+ * samples ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed
+ * caption files.</li>
+ * <li><b>{@link Renderer}</b>s that render individual components of the media. The library
+ * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
+ * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer
+ * consumes media of its corresponding type from the MediaSource being played. Renderers are
+ * injected when the player is created.</li>
+ * <li>A <b>{@link TrackSelector}</b> that selects tracks provided by the MediaSource to be
+ * consumed by each of the available Renderers. The library provides a default implementation
+ * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when
+ * the player is created.</li>
+ * <li>A <b>{@link LoadControl}</b> that controls when the MediaSource buffers more media, and how
+ * much media is buffered. The library provides a default implementation
+ * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the
+ * player is created.</li>
+ * </ul>
+ * <p>An ExoPlayer can be built using the default components provided by the library, but may also
+ * be built using custom implementations if non-standard behaviors are required. For example a
+ * custom LoadControl could be injected to change the player's buffering strategy, or a custom
+ * Renderer could be injected to use a video codec not supported natively by Android.
+ *
+ * <p>The concept of injecting components that implement pieces of player functionality is present
+ * throughout the library. The default component implementations listed above delegate work to
+ * further injected components. This allows many sub-components to be individually replaced with
+ * custom implementations. For example the default MediaSource implementations require one or more
+ * {@link DataSource} factories to be injected via their constructors. By providing a custom factory
+ * it's possible to load data from a non-standard source or through a different network stack.
+ *
+ * <h3>Threading model</h3>
+ * <p>The figure below shows ExoPlayer's threading model.</p>
+ * <p align="center">
+ * <img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading model">
+ * </p>
+ *
+ * <ul>
+ * <li>It is recommended that ExoPlayer instances are created and accessed from a single application
+ * thread. The application's main thread is ideal. Accessing an instance from multiple threads is
+ * discouraged, however if an application does wish to do this then it may do so provided that it
+ * ensures accesses are synchronized.</li>
+ * <li>Registered listeners are called on the thread that created the ExoPlayer instance.</li>
+ * <li>An internal playback thread is responsible for playback. Injected player components such as
+ * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
+ * thread.</li>
+ * <li>When the application performs an operation on the player, for example a seek, a message is
+ * delivered to the internal playback thread via a message queue. The internal playback thread
+ * consumes messages from the queue and performs the corresponding operations. Similarly, when a
+ * playback event occurs on the internal playback thread, a message is delivered to the application
+ * thread via a second message queue. The application thread consumes messages from the queue,
+ * updating the application visible state and calling corresponding listener methods.</li>
+ * <li>Injected player components may use additional background threads. For example a MediaSource
+ * may use a background thread to load data. These are implementation specific.</li>
+ * </ul>
+ */
+public interface ExoPlayer {
+
+ /**
+ * Listener of changes in player state.
+ */
+ interface EventListener {
+
+ /**
+ * Called when the timeline and/or manifest has been refreshed.
+ * <p>
+ * Note that if the timeline has changed then a position discontinuity may also have occurred.
+ * For example the current period index may have changed as a result of periods being added or
+ * removed from the timeline. The will <em>not</em> be reported via a separate call to
+ * {@link #onPositionDiscontinuity()}.
+ *
+ * @param timeline The latest timeline. Never null, but may be empty.
+ * @param manifest The latest manifest. May be null.
+ */
+ void onTimelineChanged(Timeline timeline, Object manifest);
+
+ /**
+ * Called when the available or selected tracks change.
+ *
+ * @param trackGroups The available tracks. Never null, but may be of length zero.
+ * @param trackSelections The track selections for each {@link Renderer}. Never null and always
+ * of length {@link #getRendererCount()}, but may contain null elements.
+ */
+ void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+
+ /**
+ * Called when the player starts or stops loading the source.
+ *
+ * @param isLoading Whether the source is currently being loaded.
+ */
+ void onLoadingChanged(boolean isLoading);
+
+ /**
+ * Called when the value returned from either {@link #getPlayWhenReady()} or
+ * {@link #getPlaybackState()} changes.
+ *
+ * @param playWhenReady Whether playback will proceed when ready.
+ * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
+ * interface.
+ */
+ void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+
+ /**
+ * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
+ * immediately after this method is called. The player instance can still be used, and
+ * {@link #release()} must still be called on the player should it no longer be required.
+ *
+ * @param error The error.
+ */
+ void onPlayerError(ExoPlaybackException error);
+
+ /**
+ * Called when a position discontinuity occurs without a change to the timeline. A position
+ * discontinuity occurs when the current window or period index changes (as a result of playback
+ * transitioning from one period in the timeline to the next), or when the playback position
+ * jumps within the period currently being played (as a result of a seek being performed, or
+ * when the source introduces a discontinuity internally).
+ * <p>
+ * When a position discontinuity occurs as a result of a change to the timeline this method is
+ * <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
+ */
+ void onPositionDiscontinuity();
+
+ /**
+ * Called when the current playback parameters change. The playback parameters may change due to
+ * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself
+ * may change them (for example, if audio playback switches to passthrough mode, where speed
+ * adjustment is no longer possible).
+ *
+ * @param playbackParameters The playback parameters.
+ */
+ void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
+
+ }
+
+ /**
+ * A component of an {@link ExoPlayer} that can receive messages on the playback thread.
+ * <p>
+ * Messages can be delivered to a component via {@link #sendMessages} and
+ * {@link #blockingSendMessages}.
+ */
+ interface ExoPlayerComponent {
+
+ /**
+ * Handles a message delivered to the component. Called on the playback thread.
+ *
+ * @param messageType The message type.
+ * @param message The message.
+ * @throws ExoPlaybackException If an error occurred whilst handling the message.
+ */
+ void handleMessage(int messageType, Object message) throws ExoPlaybackException;
+
+ }
+
+ /**
+ * Defines a message and a target {@link ExoPlayerComponent} to receive it.
+ */
+ final class ExoPlayerMessage {
+
+ /**
+ * The target to receive the message.
+ */
+ public final ExoPlayerComponent target;
+ /**
+ * The type of the message.
+ */
+ public final int messageType;
+ /**
+ * The message.
+ */
+ public final Object message;
+
+ /**
+ * @param target The target of the message.
+ * @param messageType The message type.
+ * @param message The message.
+ */
+ public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) {
+ this.target = target;
+ this.messageType = messageType;
+ this.message = message;
+ }
+
+ }
+
+ /**
+ * The player does not have a source to play, so it is neither buffering nor ready to play.
+ */
+ int STATE_IDLE = 1;
+ /**
+ * The player not able to immediately play from the current position. The cause is
+ * {@link Renderer} specific, but this state typically occurs when more data needs to be
+ * loaded to be ready to play, or more data needs to be buffered for playback to resume.
+ */
+ int STATE_BUFFERING = 2;
+ /**
+ * The player is able to immediately play from the current position. The player will be playing if
+ * {@link #getPlayWhenReady()} returns true, and paused otherwise.
+ */
+ int STATE_READY = 3;
+ /**
+ * The player has finished playing the media.
+ */
+ int STATE_ENDED = 4;
+
+ /**
+ * Register a listener to receive events from the player. The listener's methods will be called on
+ * the thread that was used to construct the player.
+ *
+ * @param listener The listener to register.
+ */
+ void addListener(EventListener listener);
+
+ /**
+ * Unregister a listener. The listener will no longer receive events from the player.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeListener(EventListener listener);
+
+ /**
+ * Returns the current state of the player.
+ *
+ * @return One of the {@code STATE} constants defined in this interface.
+ */
+ int getPlaybackState();
+
+ /**
+ * Prepares the player to play the provided {@link MediaSource}. Equivalent to
+ * {@code prepare(mediaSource, true, true)}.
+ */
+ void prepare(MediaSource mediaSource);
+
+ /**
+ * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback
+ * position the default position in the first {@link Timeline.Window}.
+ *
+ * @param mediaSource The {@link MediaSource} to play.
+ * @param resetPosition Whether the playback position should be reset to the default position in
+ * the first {@link Timeline.Window}. If false, playback will start from the position defined
+ * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
+ * @param resetState Whether the timeline, manifest, tracks and track selections should be reset.
+ * Should be true unless the player is being prepared to play the same media as it was playing
+ * previously (e.g. if playback failed and is being retried).
+ */
+ void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
+
+ /**
+ * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+ * <p>
+ * If the player is already in the ready state then this method can be used to pause and resume
+ * playback.
+ *
+ * @param playWhenReady Whether playback should proceed when ready.
+ */
+ void setPlayWhenReady(boolean playWhenReady);
+
+ /**
+ * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+ *
+ * @return Whether playback will proceed when ready.
+ */
+ boolean getPlayWhenReady();
+
+ /**
+ * Whether the player is currently loading the source.
+ *
+ * @return Whether the player is currently loading the source.
+ */
+ boolean isLoading();
+
+ /**
+ * Seeks to the default position associated with the current window. The position can depend on
+ * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
+ * be the live edge of the window. For other streams it will typically be the start of the window.
+ */
+ void seekToDefaultPosition();
+
+ /**
+ * Seeks to the default position associated with the specified window. The position can depend on
+ * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
+ * be the live edge of the window. For other streams it will typically be the start of the window.
+ *
+ * @param windowIndex The index of the window whose associated default position should be seeked
+ * to.
+ */
+ void seekToDefaultPosition(int windowIndex);
+
+ /**
+ * Seeks to a position specified in milliseconds in the current window.
+ *
+ * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to
+ * the window's default position.
+ */
+ void seekTo(long positionMs);
+
+ /**
+ * Seeks to a position specified in milliseconds in the specified window.
+ *
+ * @param windowIndex The index of the window.
+ * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
+ * the window's default position.
+ */
+ void seekTo(int windowIndex, long positionMs);
+
+ /**
+ * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
+ * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
+ * <p>
+ * Playback parameters changes may cause the player to buffer.
+ * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever
+ * the currently active playback parameters change. When that listener is called, the parameters
+ * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch
+ * may be out of range, in which case they are constrained to a set of permitted values. If it is
+ * not possible to change the playback parameters, the listener will not be invoked.
+ *
+ * @param playbackParameters The playback parameters, or {@code null} to use the defaults.
+ */
+ void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the currently active playback parameters.
+ *
+ * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
+ */
+ PlaybackParameters getPlaybackParameters();
+
+ /**
+ * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
+ * is to pause playback.
+ * <p>
+ * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
+ * player instance can still be used, and {@link #release()} must still be called on the player if
+ * it's no longer required.
+ * <p>
+ * Calling this method does not reset the playback position.
+ */
+ void stop();
+
+ /**
+ * Releases the player. This method must be called when the player is no longer required. The
+ * player must not be used after calling this method.
+ */
+ void release();
+
+ /**
+ * Sends messages to their target components. The messages are delivered on the playback thread.
+ * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player
+ * as an error.
+ *
+ * @param messages The messages to be sent.
+ */
+ void sendMessages(ExoPlayerMessage... messages);
+
+ /**
+ * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have
+ * been delivered.
+ *
+ * @param messages The messages to be sent.
+ */
+ void blockingSendMessages(ExoPlayerMessage... messages);
+
+ /**
+ * Returns the number of renderers.
+ */
+ int getRendererCount();
+
+ /**
+ * Returns the track type that the renderer at a given index handles.
+ *
+ * @see Renderer#getTrackType()
+ * @param index The index of the renderer.
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ int getRendererType(int index);
+
+ /**
+ * Returns the available track groups.
+ */
+ TrackGroupArray getCurrentTrackGroups();
+
+ /**
+ * Returns the current track selections for each renderer.
+ */
+ TrackSelectionArray getCurrentTrackSelections();
+
+ /**
+ * Returns the current manifest. The type depends on the {@link MediaSource} passed to
+ * {@link #prepare}. May be null.
+ */
+ Object getCurrentManifest();
+
+ /**
+ * Returns the current {@link Timeline}. Never null, but may be empty.
+ */
+ Timeline getCurrentTimeline();
+
+ /**
+ * Returns the index of the period currently being played.
+ */
+ int getCurrentPeriodIndex();
+
+ /**
+ * Returns the index of the window currently being played.
+ */
+ int getCurrentWindowIndex();
+
+ /**
+ * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the
+ * duration is not known.
+ */
+ long getDuration();
+
+ /**
+ * Returns the playback position in the current window, in milliseconds.
+ */
+ long getCurrentPosition();
+
+ /**
+ * Returns an estimate of the position in the current window up to which data is buffered, in
+ * milliseconds.
+ */
+ long getBufferedPosition();
+
+ /**
+ * Returns an estimate of the percentage in the current window up to which data is buffered, or 0
+ * if no estimate is available.
+ */
+ int getBufferedPercentage();
+
+ /**
+ * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
+ * empty.
+ *
+ * @see Timeline.Window#isDynamic
+ */
+ boolean isCurrentWindowDynamic();
+
+ /**
+ * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
+ * empty.
+ *
+ * @see Timeline.Window#isSeekable
+ */
+ boolean isCurrentWindowSeekable();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -0,0 +1,174 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.content.Context;
+import android.os.Looper;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+
+/**
+ * A factory for {@link ExoPlayer} instances.
+ */
+public final class ExoPlayerFactory {
+
+ private ExoPlayerFactory() {}
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ */
+ @Deprecated
+ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+ LoadControl loadControl) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}. Available extension renderers are not used.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ */
+ @Deprecated
+ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+ LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param extensionRendererMode The extension renderer mode, which determines if and how available
+ * extension renderers are used. Note that extensions must be included in the application
+ * build for them to be considered available.
+ * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ */
+ @Deprecated
+ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+ LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
+ extensionRendererMode);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+ * will not be used for DRM protected playbacks.
+ * @param extensionRendererMode The extension renderer mode, which determines if and how available
+ * extension renderers are used. Note that extensions must be included in the application
+ * build for them to be considered available.
+ * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
+ * seamlessly join an ongoing playback.
+ * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+ */
+ @Deprecated
+ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+ LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
+ long allowedVideoJoiningTimeMs) {
+ RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
+ extensionRendererMode, allowedVideoJoiningTimeMs);
+ return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param context A {@link Context}.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ */
+ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {
+ return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector);
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ */
+ public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
+ TrackSelector trackSelector) {
+ return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl());
+ }
+
+ /**
+ * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ */
+ public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
+ TrackSelector trackSelector, LoadControl loadControl) {
+ return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl);
+ }
+
+ /**
+ * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param renderers The {@link Renderer}s that will be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ */
+ public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
+ return newInstance(renderers, trackSelector, new DefaultLoadControl());
+ }
+
+ /**
+ * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
+ * {@link Looper}.
+ *
+ * @param renderers The {@link Renderer}s that will be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ */
+ public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
+ LoadControl loadControl) {
+ return new ExoPlayerImpl(renderers, trackSelector, loadControl);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -0,0 +1,442 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}.
+ */
+/* package */ final class ExoPlayerImpl implements ExoPlayer {
+
+ private static final String TAG = "ExoPlayerImpl";
+
+ private final Renderer[] renderers;
+ private final TrackSelector trackSelector;
+ private final TrackSelectionArray emptyTrackSelections;
+ private final Handler eventHandler;
+ private final ExoPlayerImplInternal internalPlayer;
+ private final CopyOnWriteArraySet<EventListener> listeners;
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+
+ private boolean tracksSelected;
+ private boolean playWhenReady;
+ private int playbackState;
+ private int pendingSeekAcks;
+ private int pendingPrepareAcks;
+ private boolean isLoading;
+ private Timeline timeline;
+ private Object manifest;
+ private TrackGroupArray trackGroups;
+ private TrackSelectionArray trackSelections;
+ private PlaybackParameters playbackParameters;
+
+ // Playback information when there is no pending seek/set source operation.
+ private PlaybackInfo playbackInfo;
+
+ // Playback information when there is a pending seek/set source operation.
+ private int maskingWindowIndex;
+ private int maskingPeriodIndex;
+ private long maskingWindowPositionMs;
+
+ /**
+ * Constructs an instance. Must be called from a thread that has an associated {@link Looper}.
+ *
+ * @param renderers The {@link Renderer}s that will be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ */
+ @SuppressLint("HandlerLeak")
+ public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
+ Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]");
+ Assertions.checkState(renderers.length > 0);
+ this.renderers = Assertions.checkNotNull(renderers);
+ this.trackSelector = Assertions.checkNotNull(trackSelector);
+ this.playWhenReady = false;
+ this.playbackState = STATE_IDLE;
+ this.listeners = new CopyOnWriteArraySet<>();
+ emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
+ timeline = Timeline.EMPTY;
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ trackGroups = TrackGroupArray.EMPTY;
+ trackSelections = emptyTrackSelections;
+ playbackParameters = PlaybackParameters.DEFAULT;
+ eventHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ ExoPlayerImpl.this.handleEvent(msg);
+ }
+ };
+ playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
+ internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
+ eventHandler, playbackInfo, this);
+ }
+
+ @Override
+ public void addListener(EventListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(EventListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public int getPlaybackState() {
+ return playbackState;
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource) {
+ prepare(mediaSource, true, true);
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ if (resetState) {
+ if (!timeline.isEmpty() || manifest != null) {
+ timeline = Timeline.EMPTY;
+ manifest = null;
+ for (EventListener listener : listeners) {
+ listener.onTimelineChanged(timeline, manifest);
+ }
+ }
+ if (tracksSelected) {
+ tracksSelected = false;
+ trackGroups = TrackGroupArray.EMPTY;
+ trackSelections = emptyTrackSelections;
+ trackSelector.onSelectionActivated(null);
+ for (EventListener listener : listeners) {
+ listener.onTracksChanged(trackGroups, trackSelections);
+ }
+ }
+ }
+ pendingPrepareAcks++;
+ internalPlayer.prepare(mediaSource, resetPosition);
+ }
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ if (this.playWhenReady != playWhenReady) {
+ this.playWhenReady = playWhenReady;
+ internalPlayer.setPlayWhenReady(playWhenReady);
+ for (EventListener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ }
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ return playWhenReady;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return isLoading;
+ }
+
+ @Override
+ public void seekToDefaultPosition() {
+ seekToDefaultPosition(getCurrentWindowIndex());
+ }
+
+ @Override
+ public void seekToDefaultPosition(int windowIndex) {
+ seekTo(windowIndex, C.TIME_UNSET);
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ seekTo(getCurrentWindowIndex(), positionMs);
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
+ throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
+ }
+ pendingSeekAcks++;
+ maskingWindowIndex = windowIndex;
+ if (timeline.isEmpty()) {
+ maskingPeriodIndex = 0;
+ } else {
+ timeline.getWindow(windowIndex, window);
+ long resolvedPositionMs =
+ positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs;
+ int periodIndex = window.firstPeriodIndex;
+ long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs);
+ long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
+ while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+ && periodIndex < window.lastPeriodIndex) {
+ periodPositionUs -= periodDurationUs;
+ periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
+ }
+ maskingPeriodIndex = periodIndex;
+ }
+ if (positionMs == C.TIME_UNSET) {
+ maskingWindowPositionMs = 0;
+ internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET);
+ } else {
+ maskingWindowPositionMs = positionMs;
+ internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
+ for (EventListener listener : listeners) {
+ listener.onPositionDiscontinuity();
+ }
+ }
+ }
+
+ @Override
+ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ if (playbackParameters == null) {
+ playbackParameters = PlaybackParameters.DEFAULT;
+ }
+ internalPlayer.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+ @Override
+ public void stop() {
+ internalPlayer.stop();
+ }
+
+ @Override
+ public void release() {
+ internalPlayer.release();
+ eventHandler.removeCallbacksAndMessages(null);
+ }
+
+ @Override
+ public void sendMessages(ExoPlayerMessage... messages) {
+ internalPlayer.sendMessages(messages);
+ }
+
+ @Override
+ public void blockingSendMessages(ExoPlayerMessage... messages) {
+ internalPlayer.blockingSendMessages(messages);
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ if (timeline.isEmpty() || pendingSeekAcks > 0) {
+ return maskingPeriodIndex;
+ } else {
+ return playbackInfo.periodIndex;
+ }
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ if (timeline.isEmpty() || pendingSeekAcks > 0) {
+ return maskingWindowIndex;
+ } else {
+ return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex;
+ }
+ }
+
+ @Override
+ public long getDuration() {
+ if (timeline.isEmpty()) {
+ return C.TIME_UNSET;
+ }
+ return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ if (timeline.isEmpty() || pendingSeekAcks > 0) {
+ return maskingWindowPositionMs;
+ } else {
+ timeline.getPeriod(playbackInfo.periodIndex, period);
+ return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs);
+ }
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ // TODO - Implement this properly.
+ if (timeline.isEmpty() || pendingSeekAcks > 0) {
+ return maskingWindowPositionMs;
+ } else {
+ timeline.getPeriod(playbackInfo.periodIndex, period);
+ return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs);
+ }
+ }
+
+ @Override
+ public int getBufferedPercentage() {
+ if (timeline.isEmpty()) {
+ return 0;
+ }
+ long bufferedPosition = getBufferedPosition();
+ long duration = getDuration();
+ return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0
+ : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
+ }
+
+ @Override
+ public boolean isCurrentWindowDynamic() {
+ return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
+ }
+
+ @Override
+ public boolean isCurrentWindowSeekable() {
+ return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
+ }
+
+ @Override
+ public int getRendererCount() {
+ return renderers.length;
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ return renderers[index].getTrackType();
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ return trackGroups;
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return trackSelections;
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ return timeline;
+ }
+
+ @Override
+ public Object getCurrentManifest() {
+ return manifest;
+ }
+
+ // Not private so it can be called from an inner class without going through a thunk method.
+ /* package */ void handleEvent(Message msg) {
+ switch (msg.what) {
+ case ExoPlayerImplInternal.MSG_PREPARE_ACK: {
+ pendingPrepareAcks--;
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
+ playbackState = msg.arg1;
+ for (EventListener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_LOADING_CHANGED: {
+ isLoading = msg.arg1 != 0;
+ for (EventListener listener : listeners) {
+ listener.onLoadingChanged(isLoading);
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
+ if (pendingPrepareAcks == 0) {
+ TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
+ tracksSelected = true;
+ trackGroups = trackSelectorResult.groups;
+ trackSelections = trackSelectorResult.selections;
+ trackSelector.onSelectionActivated(trackSelectorResult.info);
+ for (EventListener listener : listeners) {
+ listener.onTracksChanged(trackGroups, trackSelections);
+ }
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_SEEK_ACK: {
+ if (--pendingSeekAcks == 0) {
+ playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+ if (msg.arg1 != 0) {
+ for (EventListener listener : listeners) {
+ listener.onPositionDiscontinuity();
+ }
+ }
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: {
+ if (pendingSeekAcks == 0) {
+ playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+ for (EventListener listener : listeners) {
+ listener.onPositionDiscontinuity();
+ }
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
+ SourceInfo sourceInfo = (SourceInfo) msg.obj;
+ pendingSeekAcks -= sourceInfo.seekAcks;
+ if (pendingPrepareAcks == 0) {
+ timeline = sourceInfo.timeline;
+ manifest = sourceInfo.manifest;
+ playbackInfo = sourceInfo.playbackInfo;
+ for (EventListener listener : listeners) {
+ listener.onTimelineChanged(timeline, manifest);
+ }
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: {
+ PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
+ if (!this.playbackParameters.equals(playbackParameters)) {
+ this.playbackParameters = playbackParameters;
+ for (EventListener listener : listeners) {
+ listener.onPlaybackParametersChanged(playbackParameters);
+ }
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_ERROR: {
+ ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
+ for (EventListener listener : listeners) {
+ listener.onPlayerError(exception);
+ }
+ break;
+ }
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -0,0 +1,1573 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.StandaloneMediaClock;
+import com.google.android.exoplayer2.util.TraceUtil;
+import java.io.IOException;
+
+/**
+ * Implements the internal behavior of {@link ExoPlayerImpl}.
+ */
+/* package */ final class ExoPlayerImplInternal implements Handler.Callback,
+ MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener {
+
+ /**
+ * Playback position information which is read on the application's thread by
+ * {@link ExoPlayerImpl} and read/written internally on the player's thread.
+ */
+ public static final class PlaybackInfo {
+
+ public final int periodIndex;
+ public final long startPositionUs;
+
+ public volatile long positionUs;
+ public volatile long bufferedPositionUs;
+
+ public PlaybackInfo(int periodIndex, long startPositionUs) {
+ this.periodIndex = periodIndex;
+ this.startPositionUs = startPositionUs;
+ positionUs = startPositionUs;
+ bufferedPositionUs = startPositionUs;
+ }
+
+ public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
+ PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs);
+ playbackInfo.positionUs = positionUs;
+ playbackInfo.bufferedPositionUs = bufferedPositionUs;
+ return playbackInfo;
+ }
+
+ }
+
+ public static final class SourceInfo {
+
+ public final Timeline timeline;
+ public final Object manifest;
+ public final PlaybackInfo playbackInfo;
+ public final int seekAcks;
+
+ public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) {
+ this.timeline = timeline;
+ this.manifest = manifest;
+ this.playbackInfo = playbackInfo;
+ this.seekAcks = seekAcks;
+ }
+
+ }
+
+ private static final String TAG = "ExoPlayerImplInternal";
+
+ // External messages
+ public static final int MSG_PREPARE_ACK = 0;
+ public static final int MSG_STATE_CHANGED = 1;
+ public static final int MSG_LOADING_CHANGED = 2;
+ public static final int MSG_TRACKS_CHANGED = 3;
+ public static final int MSG_SEEK_ACK = 4;
+ public static final int MSG_POSITION_DISCONTINUITY = 5;
+ public static final int MSG_SOURCE_INFO_REFRESHED = 6;
+ public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 7;
+ public static final int MSG_ERROR = 8;
+
+ // Internal messages
+ private static final int MSG_PREPARE = 0;
+ private static final int MSG_SET_PLAY_WHEN_READY = 1;
+ private static final int MSG_DO_SOME_WORK = 2;
+ private static final int MSG_SEEK_TO = 3;
+ private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
+ private static final int MSG_STOP = 5;
+ private static final int MSG_RELEASE = 6;
+ private static final int MSG_REFRESH_SOURCE_INFO = 7;
+ private static final int MSG_PERIOD_PREPARED = 8;
+ private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
+ private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
+ private static final int MSG_CUSTOM = 11;
+
+ private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
+ private static final int RENDERING_INTERVAL_MS = 10;
+ private static final int IDLE_INTERVAL_MS = 1000;
+
+ /**
+ * Limits the maximum number of periods to buffer ahead of the current playing period. The
+ * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
+ * small periods to be buffered if the period count were not limited.
+ */
+ private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
+
+ /**
+ * Offset added to all sample timestamps read by renderers to make them non-negative. This is
+ * provided for convenience of sources that may return negative timestamps due to prerolling
+ * samples from a keyframe before their first sample with timestamp zero, so it must be set to a
+ * value greater than or equal to the maximum key-frame interval in seekable periods.
+ */
+ private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000;
+
+ private final Renderer[] renderers;
+ private final RendererCapabilities[] rendererCapabilities;
+ private final TrackSelector trackSelector;
+ private final LoadControl loadControl;
+ private final StandaloneMediaClock standaloneMediaClock;
+ private final Handler handler;
+ private final HandlerThread internalPlaybackThread;
+ private final Handler eventHandler;
+ private final ExoPlayer player;
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+
+ private PlaybackInfo playbackInfo;
+ private PlaybackParameters playbackParameters;
+ private Renderer rendererMediaClockSource;
+ private MediaClock rendererMediaClock;
+ private MediaSource mediaSource;
+ private Renderer[] enabledRenderers;
+ private boolean released;
+ private boolean playWhenReady;
+ private boolean rebuffering;
+ private boolean isLoading;
+ private int state;
+ private int customMessagesSent;
+ private int customMessagesProcessed;
+ private long elapsedRealtimeUs;
+
+ private int pendingInitialSeekCount;
+ private SeekPosition pendingSeekPosition;
+ private long rendererPositionUs;
+
+ private MediaPeriodHolder loadingPeriodHolder;
+ private MediaPeriodHolder readingPeriodHolder;
+ private MediaPeriodHolder playingPeriodHolder;
+
+ private Timeline timeline;
+
+ public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
+ LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
+ PlaybackInfo playbackInfo, ExoPlayer player) {
+ this.renderers = renderers;
+ this.trackSelector = trackSelector;
+ this.loadControl = loadControl;
+ this.playWhenReady = playWhenReady;
+ this.eventHandler = eventHandler;
+ this.state = ExoPlayer.STATE_IDLE;
+ this.playbackInfo = playbackInfo;
+ this.player = player;
+
+ rendererCapabilities = new RendererCapabilities[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ renderers[i].setIndex(i);
+ rendererCapabilities[i] = renderers[i].getCapabilities();
+ }
+ standaloneMediaClock = new StandaloneMediaClock();
+ enabledRenderers = new Renderer[0];
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ trackSelector.init(this);
+ playbackParameters = PlaybackParameters.DEFAULT;
+
+ // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
+ // not normally change to this priority" is incorrect.
+ internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler",
+ Process.THREAD_PRIORITY_AUDIO);
+ internalPlaybackThread.start();
+ handler = new Handler(internalPlaybackThread.getLooper(), this);
+ }
+
+ public void prepare(MediaSource mediaSource, boolean resetPosition) {
+ handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
+ .sendToTarget();
+ }
+
+ public void setPlayWhenReady(boolean playWhenReady) {
+ handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
+ }
+
+ public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
+ handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
+ .sendToTarget();
+ }
+
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
+ }
+
+ public void stop() {
+ handler.sendEmptyMessage(MSG_STOP);
+ }
+
+ public void sendMessages(ExoPlayerMessage... messages) {
+ if (released) {
+ Log.w(TAG, "Ignoring messages sent after release.");
+ return;
+ }
+ customMessagesSent++;
+ handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+ }
+
+ public synchronized void blockingSendMessages(ExoPlayerMessage... messages) {
+ if (released) {
+ Log.w(TAG, "Ignoring messages sent after release.");
+ return;
+ }
+ int messageNumber = customMessagesSent++;
+ handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+ while (customMessagesProcessed <= messageNumber) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ public synchronized void release() {
+ if (released) {
+ return;
+ }
+ handler.sendEmptyMessage(MSG_RELEASE);
+ while (!released) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ internalPlaybackThread.quit();
+ }
+
+ // MediaSource.Listener implementation.
+
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, Pair.create(timeline, manifest)).sendToTarget();
+ }
+
+ // MediaPeriod.Callback implementation.
+
+ @Override
+ public void onPrepared(MediaPeriod source) {
+ handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();
+ }
+
+ // TrackSelector.InvalidationListener implementation.
+
+ @Override
+ public void onTrackSelectionsInvalidated() {
+ handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
+ }
+
+ // Handler.Callback implementation.
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean handleMessage(Message msg) {
+ try {
+ switch (msg.what) {
+ case MSG_PREPARE: {
+ prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
+ return true;
+ }
+ case MSG_SET_PLAY_WHEN_READY: {
+ setPlayWhenReadyInternal(msg.arg1 != 0);
+ return true;
+ }
+ case MSG_DO_SOME_WORK: {
+ doSomeWork();
+ return true;
+ }
+ case MSG_SEEK_TO: {
+ seekToInternal((SeekPosition) msg.obj);
+ return true;
+ }
+ case MSG_SET_PLAYBACK_PARAMETERS: {
+ setPlaybackParametersInternal((PlaybackParameters) msg.obj);
+ return true;
+ }
+ case MSG_STOP: {
+ stopInternal();
+ return true;
+ }
+ case MSG_RELEASE: {
+ releaseInternal();
+ return true;
+ }
+ case MSG_PERIOD_PREPARED: {
+ handlePeriodPrepared((MediaPeriod) msg.obj);
+ return true;
+ }
+ case MSG_REFRESH_SOURCE_INFO: {
+ handleSourceInfoRefreshed((Pair<Timeline, Object>) msg.obj);
+ return true;
+ }
+ case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: {
+ handleContinueLoadingRequested((MediaPeriod) msg.obj);
+ return true;
+ }
+ case MSG_TRACK_SELECTION_INVALIDATED: {
+ reselectTracksInternal();
+ return true;
+ }
+ case MSG_CUSTOM: {
+ sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
+ return true;
+ }
+ default:
+ return false;
+ }
+ } catch (ExoPlaybackException e) {
+ Log.e(TAG, "Renderer error.", e);
+ eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+ stopInternal();
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Source error.", e);
+ eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
+ stopInternal();
+ return true;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Internal runtime error.", e);
+ eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
+ .sendToTarget();
+ stopInternal();
+ return true;
+ }
+ }
+
+ // Private methods.
+
+ private void setState(int state) {
+ if (this.state != state) {
+ this.state = state;
+ eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
+ }
+ }
+
+ private void setIsLoading(boolean isLoading) {
+ if (this.isLoading != isLoading) {
+ this.isLoading = isLoading;
+ eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget();
+ }
+ }
+
+ private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
+ eventHandler.sendEmptyMessage(MSG_PREPARE_ACK);
+ resetInternal(true);
+ loadControl.onPrepared();
+ if (resetPosition) {
+ playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+ }
+ this.mediaSource = mediaSource;
+ mediaSource.prepareSource(player, true, this);
+ setState(ExoPlayer.STATE_BUFFERING);
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+
+ private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+ rebuffering = false;
+ this.playWhenReady = playWhenReady;
+ if (!playWhenReady) {
+ stopRenderers();
+ updatePlaybackPositions();
+ } else {
+ if (state == ExoPlayer.STATE_READY) {
+ startRenderers();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ } else if (state == ExoPlayer.STATE_BUFFERING) {
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ }
+ }
+
+ private void startRenderers() throws ExoPlaybackException {
+ rebuffering = false;
+ standaloneMediaClock.start();
+ for (Renderer renderer : enabledRenderers) {
+ renderer.start();
+ }
+ }
+
+ private void stopRenderers() throws ExoPlaybackException {
+ standaloneMediaClock.stop();
+ for (Renderer renderer : enabledRenderers) {
+ ensureStopped(renderer);
+ }
+ }
+
+ private void updatePlaybackPositions() throws ExoPlaybackException {
+ if (playingPeriodHolder == null) {
+ return;
+ }
+
+ // Update the playback position.
+ long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();
+ if (periodPositionUs != C.TIME_UNSET) {
+ resetRendererPosition(periodPositionUs);
+ } else {
+ if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) {
+ rendererPositionUs = rendererMediaClock.getPositionUs();
+ standaloneMediaClock.setPositionUs(rendererPositionUs);
+ } else {
+ rendererPositionUs = standaloneMediaClock.getPositionUs();
+ }
+ periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
+ }
+ playbackInfo.positionUs = periodPositionUs;
+ elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
+
+ // Update the buffered position.
+ long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
+ : playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
+ playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
+ ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs()
+ : bufferedPositionUs;
+ }
+
+ private void doSomeWork() throws ExoPlaybackException, IOException {
+ long operationStartTimeMs = SystemClock.elapsedRealtime();
+ updatePeriods();
+ if (playingPeriodHolder == null) {
+ // We're still waiting for the first period to be prepared.
+ maybeThrowPeriodPrepareError();
+ scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
+ return;
+ }
+
+ TraceUtil.beginSection("doSomeWork");
+
+ updatePlaybackPositions();
+ playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
+
+ boolean allRenderersEnded = true;
+ boolean allRenderersReadyOrEnded = true;
+ for (Renderer renderer : enabledRenderers) {
+ // TODO: Each renderer should return the maximum delay before which it wishes to be called
+ // again. The minimum of these values should then be used as the delay before the next
+ // invocation of this method.
+ renderer.render(rendererPositionUs, elapsedRealtimeUs);
+ allRenderersEnded = allRenderersEnded && renderer.isEnded();
+ // Determine whether the renderer is ready (or ended). If it's not, throw an error that's
+ // preventing the renderer from making progress, if such an error exists.
+ boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded();
+ if (!rendererReadyOrEnded) {
+ renderer.maybeThrowStreamError();
+ }
+ allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
+ }
+
+ if (!allRenderersReadyOrEnded) {
+ maybeThrowPeriodPrepareError();
+ }
+
+ // The standalone media clock never changes playback parameters, so just check the renderer.
+ if (rendererMediaClock != null) {
+ PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters();
+ if (!playbackParameters.equals(this.playbackParameters)) {
+ // TODO: Make LoadControl, period transition position projection, adaptive track selection
+ // and potentially any time-related code in renderers take into account the playback speed.
+ this.playbackParameters = playbackParameters;
+ standaloneMediaClock.synchronize(rendererMediaClock);
+ eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters)
+ .sendToTarget();
+ }
+ }
+
+ long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
+ .getDurationUs();
+ if (allRenderersEnded
+ && (playingPeriodDurationUs == C.TIME_UNSET
+ || playingPeriodDurationUs <= playbackInfo.positionUs)
+ && playingPeriodHolder.isLast) {
+ setState(ExoPlayer.STATE_ENDED);
+ stopRenderers();
+ } else if (state == ExoPlayer.STATE_BUFFERING) {
+ boolean isNewlyReady = enabledRenderers.length > 0
+ ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering))
+ : isTimelineReady(playingPeriodDurationUs);
+ if (isNewlyReady) {
+ setState(ExoPlayer.STATE_READY);
+ if (playWhenReady) {
+ startRenderers();
+ }
+ }
+ } else if (state == ExoPlayer.STATE_READY) {
+ boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded
+ : isTimelineReady(playingPeriodDurationUs);
+ if (!isStillReady) {
+ rebuffering = playWhenReady;
+ setState(ExoPlayer.STATE_BUFFERING);
+ stopRenderers();
+ }
+ }
+
+ if (state == ExoPlayer.STATE_BUFFERING) {
+ for (Renderer renderer : enabledRenderers) {
+ renderer.maybeThrowStreamError();
+ }
+ }
+
+ if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
+ scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
+ } else if (enabledRenderers.length != 0) {
+ scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
+ } else {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ }
+
+ TraceUtil.endSection();
+ }
+
+ private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
+ long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
+ if (nextOperationDelayMs <= 0) {
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ } else {
+ handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs);
+ }
+ }
+
+ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
+ if (timeline == null) {
+ pendingInitialSeekCount++;
+ pendingSeekPosition = seekPosition;
+ return;
+ }
+
+ Pair<Integer, Long> periodPosition = resolveSeekPosition(seekPosition);
+ if (periodPosition == null) {
+ // The seek position was valid for the timeline that it was performed into, but the
+ // timeline has changed and a suitable seek position could not be resolved in the new one.
+ playbackInfo = new PlaybackInfo(0, 0);
+ eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget();
+ // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't
+ // ignored.
+ playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+ setState(ExoPlayer.STATE_ENDED);
+ // Reset, but retain the source so that it can still be used should a seek occur.
+ resetInternal(false);
+ return;
+ }
+
+ boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
+ int periodIndex = periodPosition.first;
+ long periodPositionUs = periodPosition.second;
+
+ try {
+ if (periodIndex == playbackInfo.periodIndex
+ && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
+ // Seek position equals the current position. Do nothing.
+ return;
+ }
+ long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
+ seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
+ periodPositionUs = newPeriodPositionUs;
+ } finally {
+ playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
+ eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
+ .sendToTarget();
+ }
+ }
+
+ private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
+ throws ExoPlaybackException {
+ stopRenderers();
+ rebuffering = false;
+ setState(ExoPlayer.STATE_BUFFERING);
+
+ MediaPeriodHolder newPlayingPeriodHolder = null;
+ if (playingPeriodHolder == null) {
+ // We're still waiting for the first period to be prepared.
+ if (loadingPeriodHolder != null) {
+ loadingPeriodHolder.release();
+ }
+ } else {
+ // Clear the timeline, but keep the requested period if it is already prepared.
+ MediaPeriodHolder periodHolder = playingPeriodHolder;
+ while (periodHolder != null) {
+ if (periodHolder.index == periodIndex && periodHolder.prepared) {
+ newPlayingPeriodHolder = periodHolder;
+ } else {
+ periodHolder.release();
+ }
+ periodHolder = periodHolder.next;
+ }
+ }
+
+ // Disable all the renderers if the period being played is changing, or if the renderers are
+ // reading from a period other than the one being played.
+ if (playingPeriodHolder != newPlayingPeriodHolder
+ || playingPeriodHolder != readingPeriodHolder) {
+ for (Renderer renderer : enabledRenderers) {
+ renderer.disable();
+ }
+ enabledRenderers = new Renderer[0];
+ rendererMediaClock = null;
+ rendererMediaClockSource = null;
+ playingPeriodHolder = null;
+ }
+
+ // Update the holders.
+ if (newPlayingPeriodHolder != null) {
+ newPlayingPeriodHolder.next = null;
+ loadingPeriodHolder = newPlayingPeriodHolder;
+ readingPeriodHolder = newPlayingPeriodHolder;
+ setPlayingPeriodHolder(newPlayingPeriodHolder);
+ if (playingPeriodHolder.hasEnabledTracks) {
+ periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+ }
+ resetRendererPosition(periodPositionUs);
+ maybeContinueLoading();
+ } else {
+ loadingPeriodHolder = null;
+ readingPeriodHolder = null;
+ playingPeriodHolder = null;
+ resetRendererPosition(periodPositionUs);
+ }
+
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ return periodPositionUs;
+ }
+
+ private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
+ rendererPositionUs = playingPeriodHolder == null
+ ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+ : playingPeriodHolder.toRendererTime(periodPositionUs);
+ standaloneMediaClock.setPositionUs(rendererPositionUs);
+ for (Renderer renderer : enabledRenderers) {
+ renderer.resetPosition(rendererPositionUs);
+ }
+ }
+
+ private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
+ playbackParameters = rendererMediaClock != null
+ ? rendererMediaClock.setPlaybackParameters(playbackParameters)
+ : standaloneMediaClock.setPlaybackParameters(playbackParameters);
+ this.playbackParameters = playbackParameters;
+ eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+ }
+
+ private void stopInternal() {
+ resetInternal(true);
+ loadControl.onStopped();
+ setState(ExoPlayer.STATE_IDLE);
+ }
+
+ private void releaseInternal() {
+ resetInternal(true);
+ loadControl.onReleased();
+ setState(ExoPlayer.STATE_IDLE);
+ synchronized (this) {
+ released = true;
+ notifyAll();
+ }
+ }
+
+ private void resetInternal(boolean releaseMediaSource) {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ rebuffering = false;
+ standaloneMediaClock.stop();
+ rendererMediaClock = null;
+ rendererMediaClockSource = null;
+ rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US;
+ for (Renderer renderer : enabledRenderers) {
+ try {
+ ensureStopped(renderer);
+ renderer.disable();
+ } catch (ExoPlaybackException | RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Stop failed.", e);
+ }
+ }
+ enabledRenderers = new Renderer[0];
+ releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
+ : loadingPeriodHolder);
+ loadingPeriodHolder = null;
+ readingPeriodHolder = null;
+ playingPeriodHolder = null;
+ setIsLoading(false);
+ if (releaseMediaSource) {
+ if (mediaSource != null) {
+ mediaSource.releaseSource();
+ mediaSource = null;
+ }
+ timeline = null;
+ }
+ }
+
+ private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
+ try {
+ for (ExoPlayerMessage message : messages) {
+ message.target.handleMessage(message.messageType, message.message);
+ }
+ if (mediaSource != null) {
+ // The message may have caused something to change that now requires us to do work.
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ } finally {
+ synchronized (this) {
+ customMessagesProcessed++;
+ notifyAll();
+ }
+ }
+ }
+
+ private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
+ if (renderer.getState() == Renderer.STATE_STARTED) {
+ renderer.stop();
+ }
+ }
+
+ private void reselectTracksInternal() throws ExoPlaybackException {
+ if (playingPeriodHolder == null) {
+ // We don't have tracks yet, so we don't care.
+ return;
+ }
+ // Reselect tracks on each period in turn, until the selection changes.
+ MediaPeriodHolder periodHolder = playingPeriodHolder;
+ boolean selectionsChangedForReadPeriod = true;
+ while (true) {
+ if (periodHolder == null || !periodHolder.prepared) {
+ // The reselection did not change any prepared periods.
+ return;
+ }
+ if (periodHolder.selectTracks()) {
+ // Selected tracks have changed for this period.
+ break;
+ }
+ if (periodHolder == readingPeriodHolder) {
+ // The track reselection didn't affect any period that has been read.
+ selectionsChangedForReadPeriod = false;
+ }
+ periodHolder = periodHolder.next;
+ }
+
+ if (selectionsChangedForReadPeriod) {
+ // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
+ boolean recreateStreams = readingPeriodHolder != playingPeriodHolder;
+ releasePeriodHoldersFrom(playingPeriodHolder.next);
+ playingPeriodHolder.next = null;
+ loadingPeriodHolder = playingPeriodHolder;
+ readingPeriodHolder = playingPeriodHolder;
+
+ boolean[] streamResetFlags = new boolean[renderers.length];
+ long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection(
+ playbackInfo.positionUs, recreateStreams, streamResetFlags);
+ if (periodPositionUs != playbackInfo.positionUs) {
+ playbackInfo.positionUs = periodPositionUs;
+ resetRendererPosition(periodPositionUs);
+ }
+
+ int enabledRendererCount = 0;
+ boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+ SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];
+ if (sampleStream != null) {
+ enabledRendererCount++;
+ }
+ if (rendererWasEnabledFlags[i]) {
+ if (sampleStream != renderer.getStream()) {
+ // We need to disable the renderer.
+ if (renderer == rendererMediaClockSource) {
+ // The renderer is providing the media clock.
+ if (sampleStream == null) {
+ // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
+ // over timing responsibilities.
+ standaloneMediaClock.synchronize(rendererMediaClock);
+ }
+ rendererMediaClock = null;
+ rendererMediaClockSource = null;
+ }
+ ensureStopped(renderer);
+ renderer.disable();
+ } else if (streamResetFlags[i]) {
+ // The renderer will continue to consume from its current stream, but needs to be reset.
+ renderer.resetPosition(rendererPositionUs);
+ }
+ }
+ }
+ eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult)
+ .sendToTarget();
+ enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+ } else {
+ // Release and re-prepare/buffer periods after the one whose selection changed.
+ loadingPeriodHolder = periodHolder;
+ periodHolder = loadingPeriodHolder.next;
+ while (periodHolder != null) {
+ periodHolder.release();
+ periodHolder = periodHolder.next;
+ }
+ loadingPeriodHolder.next = null;
+ if (loadingPeriodHolder.prepared) {
+ long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
+ loadingPeriodHolder.toPeriodTime(rendererPositionUs));
+ loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
+ }
+ }
+ maybeContinueLoading();
+ updatePlaybackPositions();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+
+ private boolean isTimelineReady(long playingPeriodDurationUs) {
+ return playingPeriodDurationUs == C.TIME_UNSET
+ || playbackInfo.positionUs < playingPeriodDurationUs
+ || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
+ }
+
+ private boolean haveSufficientBuffer(boolean rebuffering) {
+ long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared
+ ? loadingPeriodHolder.startPositionUs
+ : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs();
+ if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) {
+ if (loadingPeriodHolder.isLast) {
+ return true;
+ }
+ loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period)
+ .getDurationUs();
+ }
+ return loadControl.shouldStartPlayback(
+ loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs),
+ rebuffering);
+ }
+
+ private void maybeThrowPeriodPrepareError() throws IOException {
+ if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
+ && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
+ for (Renderer renderer : enabledRenderers) {
+ if (!renderer.hasReadStreamToEnd()) {
+ return;
+ }
+ }
+ loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
+ }
+ }
+
+ private void handleSourceInfoRefreshed(Pair<Timeline, Object> timelineAndManifest)
+ throws ExoPlaybackException {
+ Timeline oldTimeline = timeline;
+ timeline = timelineAndManifest.first;
+ Object manifest = timelineAndManifest.second;
+
+ int processedInitialSeekCount = 0;
+ if (oldTimeline == null) {
+ if (pendingInitialSeekCount > 0) {
+ Pair<Integer, Long> periodPosition = resolveSeekPosition(pendingSeekPosition);
+ processedInitialSeekCount = pendingInitialSeekCount;
+ pendingInitialSeekCount = 0;
+ pendingSeekPosition = null;
+ if (periodPosition == null) {
+ // The seek position was valid for the timeline that it was performed into, but the
+ // timeline has changed and a suitable seek position could not be resolved in the new one.
+ handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+ return;
+ }
+ playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
+ } else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
+ if (timeline.isEmpty()) {
+ handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+ return;
+ }
+ Pair<Integer, Long> defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
+ playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
+ }
+ }
+
+ MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
+ : loadingPeriodHolder;
+ if (periodHolder == null) {
+ // We don't have any period holders, so we're done.
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ return;
+ }
+
+ int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid);
+ if (periodIndex == C.INDEX_UNSET) {
+ // We didn't find the current period in the new timeline. Attempt to resolve a subsequent
+ // period whose window we can restart from.
+ int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline);
+ if (newPeriodIndex == C.INDEX_UNSET) {
+ // We failed to resolve a suitable restart position.
+ handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+ return;
+ }
+ // We resolved a subsequent period. Seek to the default position in the corresponding window.
+ Pair<Integer, Long> defaultPosition = getPeriodPosition(
+ timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
+ newPeriodIndex = defaultPosition.first;
+ long newPositionUs = defaultPosition.second;
+ timeline.getPeriod(newPeriodIndex, period, true);
+ // Clear the index of each holder that doesn't contain the default position. If a holder
+ // contains the default position then update its index so it can be re-used when seeking.
+ Object newPeriodUid = period.uid;
+ periodHolder.index = C.INDEX_UNSET;
+ while (periodHolder.next != null) {
+ periodHolder = periodHolder.next;
+ periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET;
+ }
+ // Actually do the seek.
+ newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs);
+ playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs);
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ return;
+ }
+
+ // The current period is in the new timeline. Update the holder and playbackInfo.
+ timeline.getPeriod(periodIndex, period);
+ boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
+ && !timeline.getWindow(period.windowIndex, window).isDynamic;
+ periodHolder.setIndex(periodIndex, isLastPeriod);
+ boolean seenReadingPeriod = periodHolder == readingPeriodHolder;
+ if (periodIndex != playbackInfo.periodIndex) {
+ playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
+ }
+
+ // If there are subsequent holders, update the index for each of them. If we find a holder
+ // that's inconsistent with the new timeline then take appropriate action.
+ while (periodHolder.next != null) {
+ MediaPeriodHolder previousPeriodHolder = periodHolder;
+ periodHolder = periodHolder.next;
+ periodIndex++;
+ timeline.getPeriod(periodIndex, period, true);
+ isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
+ && !timeline.getWindow(period.windowIndex, window).isDynamic;
+ if (periodHolder.uid.equals(period.uid)) {
+ // The holder is consistent with the new timeline. Update its index and continue.
+ periodHolder.setIndex(periodIndex, isLastPeriod);
+ seenReadingPeriod |= (periodHolder == readingPeriodHolder);
+ } else {
+ // The holder is inconsistent with the new timeline.
+ if (!seenReadingPeriod) {
+ // Renderers may have read from a period that's been removed. Seek back to the current
+ // position of the playing period to make sure none of the removed period is played.
+ periodIndex = playingPeriodHolder.index;
+ long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs);
+ playbackInfo = new PlaybackInfo(periodIndex, newPositionUs);
+ } else {
+ // Update the loading period to be the last period that's still valid, and release all
+ // subsequent periods.
+ loadingPeriodHolder = previousPeriodHolder;
+ loadingPeriodHolder.next = null;
+ // Release the rest of the timeline.
+ releasePeriodHoldersFrom(periodHolder);
+ }
+ break;
+ }
+ }
+
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ }
+
+ private void handleSourceInfoRefreshEndedPlayback(Object manifest,
+ int processedInitialSeekCount) {
+ // Set the playback position to (0,0) for notifying the eventHandler.
+ playbackInfo = new PlaybackInfo(0, 0);
+ notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+ // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored.
+ playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+ setState(ExoPlayer.STATE_ENDED);
+ // Reset, but retain the source so that it can still be used should a seek occur.
+ resetInternal(false);
+ }
+
+ private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) {
+ eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED,
+ new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget();
+ }
+
+ /**
+ * Given a period index into an old timeline, finds the first subsequent period that also exists
+ * in a new timeline. The index of this period in the new timeline is returned.
+ *
+ * @param oldPeriodIndex The index of the period in the old timeline.
+ * @param oldTimeline The old timeline.
+ * @param newTimeline The new timeline.
+ * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET}
+ * if no such period was found.
+ */
+ private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
+ Timeline newTimeline) {
+ int newPeriodIndex = C.INDEX_UNSET;
+ while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) {
+ newPeriodIndex = newTimeline.getIndexOfPeriod(
+ oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid);
+ }
+ return newPeriodIndex;
+ }
+
+ /**
+ * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the
+ * internal timeline.
+ *
+ * @param seekPosition The position to resolve.
+ * @return The resolved position, or null if resolution was not successful.
+ * @throws IllegalSeekPositionException If the window index of the seek position is outside the
+ * bounds of the timeline.
+ */
+ private Pair<Integer, Long> resolveSeekPosition(SeekPosition seekPosition) {
+ Timeline seekTimeline = seekPosition.timeline;
+ if (seekTimeline.isEmpty()) {
+ // The application performed a blind seek without a non-empty timeline (most likely based on
+ // knowledge of what the future timeline will be). Use the internal timeline.
+ seekTimeline = timeline;
+ }
+ // Map the SeekPosition to a position in the corresponding timeline.
+ Pair<Integer, Long> periodPosition;
+ try {
+ periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex,
+ seekPosition.windowPositionUs);
+ } catch (IndexOutOfBoundsException e) {
+ // The window index of the seek position was outside the bounds of the timeline.
+ throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex,
+ seekPosition.windowPositionUs);
+ }
+ if (timeline == seekTimeline) {
+ // Our internal timeline is the seek timeline, so the mapped position is correct.
+ return periodPosition;
+ }
+ // Attempt to find the mapped period in the internal timeline.
+ int periodIndex = timeline.getIndexOfPeriod(
+ seekTimeline.getPeriod(periodPosition.first, period, true).uid);
+ if (periodIndex != C.INDEX_UNSET) {
+ // We successfully located the period in the internal timeline.
+ return Pair.create(periodIndex, periodPosition.second);
+ }
+ // Try and find a subsequent period from the seek timeline in the internal timeline.
+ periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
+ if (periodIndex != C.INDEX_UNSET) {
+ // We found one. Map the SeekPosition onto the corresponding default position.
+ return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET);
+ }
+ // We didn't find one. Give up.
+ return null;
+ }
+
+ /**
+ * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline.
+ */
+ private Pair<Integer, Long> getPeriodPosition(int windowIndex, long windowPositionUs) {
+ return getPeriodPosition(timeline, windowIndex, windowPositionUs);
+ }
+
+ /**
+ * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position
+ * projection.
+ */
+ private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
+ long windowPositionUs) {
+ return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0);
+ }
+
+ /**
+ * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
+ *
+ * @param timeline The timeline containing the window.
+ * @param windowIndex The window index.
+ * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
+ * start position.
+ * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
+ * duration into the future by which the window's position should be projected.
+ * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
+ * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
+ * position could not be projected by {@code defaultPositionProjectionUs}.
+ */
+ private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
+ long windowPositionUs, long defaultPositionProjectionUs) {
+ Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
+ timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs);
+ if (windowPositionUs == C.TIME_UNSET) {
+ windowPositionUs = window.getDefaultPositionUs();
+ if (windowPositionUs == C.TIME_UNSET) {
+ return null;
+ }
+ }
+ int periodIndex = window.firstPeriodIndex;
+ long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
+ long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
+ while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+ && periodIndex < window.lastPeriodIndex) {
+ periodPositionUs -= periodDurationUs;
+ periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
+ }
+ return Pair.create(periodIndex, periodPositionUs);
+ }
+
+ private void updatePeriods() throws ExoPlaybackException, IOException {
+ if (timeline == null) {
+ // We're waiting to get information about periods.
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return;
+ }
+
+ // Update the loading period if required.
+ maybeUpdateLoadingPeriod();
+ if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
+ setIsLoading(false);
+ } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) {
+ maybeContinueLoading();
+ }
+
+ if (playingPeriodHolder == null) {
+ // We're waiting for the first period to be prepared.
+ return;
+ }
+
+ // Update the playing and reading periods.
+ while (playingPeriodHolder != readingPeriodHolder
+ && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) {
+ // All enabled renderers' streams have been read to the end, and the playback position reached
+ // the end of the playing period, so advance playback to the next period.
+ playingPeriodHolder.release();
+ setPlayingPeriodHolder(playingPeriodHolder.next);
+ playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
+ playingPeriodHolder.startPositionUs);
+ updatePlaybackPositions();
+ eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
+ }
+
+ if (readingPeriodHolder.isLast) {
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+ // Defer setting the stream as final until the renderer has actually consumed the whole
+ // stream in case of playlist changes that cause the stream to be no longer final.
+ if (sampleStream != null && renderer.getStream() == sampleStream
+ && renderer.hasReadStreamToEnd()) {
+ renderer.setCurrentStreamFinal();
+ }
+ }
+ return;
+ }
+
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+ if (renderer.getStream() != sampleStream
+ || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
+ return;
+ }
+ }
+
+ if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
+ TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+ readingPeriodHolder = readingPeriodHolder.next;
+ TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+
+ boolean initialDiscontinuity =
+ readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i);
+ if (oldSelection == null) {
+ // The renderer has no current stream and will be enabled when we play the next period.
+ } else if (initialDiscontinuity) {
+ // The new period starts with a discontinuity, so the renderer will play out all data then
+ // be disabled and re-enabled when it starts playing the next period.
+ renderer.setCurrentStreamFinal();
+ } else if (!renderer.isCurrentStreamFinal()) {
+ TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
+ RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
+ RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
+ if (newSelection != null && newConfig.equals(oldConfig)) {
+ // Replace the renderer's SampleStream so the transition to playing the next period can
+ // be seamless.
+ Format[] formats = new Format[newSelection.length()];
+ for (int j = 0; j < formats.length; j++) {
+ formats[j] = newSelection.getFormat(j);
+ }
+ renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
+ readingPeriodHolder.getRendererOffset());
+ } else {
+ // The renderer will be disabled when transitioning to playing the next period, either
+ // because there's no new selection or because a configuration change is required. Mark
+ // the SampleStream as final to play out any remaining data.
+ renderer.setCurrentStreamFinal();
+ }
+ }
+ }
+ }
+ }
+
+ private void maybeUpdateLoadingPeriod() throws IOException {
+ int newLoadingPeriodIndex;
+ if (loadingPeriodHolder == null) {
+ newLoadingPeriodIndex = playbackInfo.periodIndex;
+ } else {
+ int loadingPeriodIndex = loadingPeriodHolder.index;
+ if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered()
+ || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
+ // Either the existing loading period is the last period, or we are not ready to advance to
+ // loading the next period because it hasn't been fully buffered or its duration is unknown.
+ return;
+ }
+ if (playingPeriodHolder != null
+ && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) {
+ // We are already buffering the maximum number of periods ahead.
+ return;
+ }
+ newLoadingPeriodIndex = loadingPeriodHolder.index + 1;
+ }
+
+ if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
+ // The next period is not available yet.
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return;
+ }
+
+ long newLoadingPeriodStartPositionUs;
+ if (loadingPeriodHolder == null) {
+ newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
+ } else {
+ int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
+ if (newLoadingPeriodIndex
+ != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) {
+ // We're starting to buffer a new period in the current window. Always start from the
+ // beginning of the period.
+ newLoadingPeriodStartPositionUs = 0;
+ } else {
+ // We're starting to buffer a new window. When playback transitions to this window we'll
+ // want it to be from its default start position. The expected delay until playback
+ // transitions is equal the duration of media that's currently buffered (assuming no
+ // interruptions). Hence we project the default start position forward by the duration of
+ // the buffer, and start buffering from this point.
+ long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset()
+ + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
+ - rendererPositionUs;
+ Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex,
+ C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
+ if (defaultPosition == null) {
+ return;
+ }
+
+ newLoadingPeriodIndex = defaultPosition.first;
+ newLoadingPeriodStartPositionUs = defaultPosition.second;
+ }
+ }
+
+ long rendererPositionOffsetUs = loadingPeriodHolder == null
+ ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+ : (loadingPeriodHolder.getRendererOffset()
+ + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
+ timeline.getPeriod(newLoadingPeriodIndex, period, true);
+ boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1
+ && !timeline.getWindow(period.windowIndex, window).isDynamic;
+ MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
+ rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid,
+ newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs);
+ if (loadingPeriodHolder != null) {
+ loadingPeriodHolder.next = newPeriodHolder;
+ }
+ loadingPeriodHolder = newPeriodHolder;
+ loadingPeriodHolder.mediaPeriod.prepare(this);
+ setIsLoading(true);
+ }
+
+ private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException {
+ if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+ // Stale event.
+ return;
+ }
+ loadingPeriodHolder.handlePrepared();
+ if (playingPeriodHolder == null) {
+ // This is the first prepared period, so start playing it.
+ readingPeriodHolder = loadingPeriodHolder;
+ resetRendererPosition(readingPeriodHolder.startPositionUs);
+ setPlayingPeriodHolder(readingPeriodHolder);
+ }
+ maybeContinueLoading();
+ }
+
+ private void handleContinueLoadingRequested(MediaPeriod period) {
+ if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+ // Stale event.
+ return;
+ }
+ maybeContinueLoading();
+ }
+
+ private void maybeContinueLoading() {
+ long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0
+ : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ setIsLoading(false);
+ } else {
+ long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
+ boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs);
+ setIsLoading(continueLoading);
+ if (continueLoading) {
+ loadingPeriodHolder.needsContinueLoading = false;
+ loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs);
+ } else {
+ loadingPeriodHolder.needsContinueLoading = true;
+ }
+ }
+ }
+
+ private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) {
+ while (periodHolder != null) {
+ periodHolder.release();
+ periodHolder = periodHolder.next;
+ }
+ }
+
+ private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException {
+ if (playingPeriodHolder == periodHolder) {
+ return;
+ }
+
+ int enabledRendererCount = 0;
+ boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+ TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i);
+ if (newSelection != null) {
+ enabledRendererCount++;
+ }
+ if (rendererWasEnabledFlags[i] && (newSelection == null
+ || (renderer.isCurrentStreamFinal()
+ && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) {
+ // The renderer should be disabled before playing the next period, either because it's not
+ // needed to play the next period, or because we need to re-enable it as its current stream
+ // is final and it's not reading ahead.
+ if (renderer == rendererMediaClockSource) {
+ // Sync standaloneMediaClock so that it can take over timing responsibilities.
+ standaloneMediaClock.synchronize(rendererMediaClock);
+ rendererMediaClock = null;
+ rendererMediaClockSource = null;
+ }
+ ensureStopped(renderer);
+ renderer.disable();
+ }
+ }
+
+ playingPeriodHolder = periodHolder;
+ eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget();
+ enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+ }
+
+ private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount)
+ throws ExoPlaybackException {
+ enabledRenderers = new Renderer[enabledRendererCount];
+ enabledRendererCount = 0;
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i);
+ if (newSelection != null) {
+ enabledRenderers[enabledRendererCount++] = renderer;
+ if (renderer.getState() == Renderer.STATE_DISABLED) {
+ RendererConfiguration rendererConfiguration =
+ playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
+ // The renderer needs enabling with its new track selection.
+ boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
+ // Consider as joining only if the renderer was previously disabled.
+ boolean joining = !rendererWasEnabledFlags[i] && playing;
+ // Build an array of formats contained by the selection.
+ Format[] formats = new Format[newSelection.length()];
+ for (int j = 0; j < formats.length; j++) {
+ formats[j] = newSelection.getFormat(j);
+ }
+ // Enable the renderer.
+ renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i],
+ rendererPositionUs, joining, playingPeriodHolder.getRendererOffset());
+ MediaClock mediaClock = renderer.getMediaClock();
+ if (mediaClock != null) {
+ if (rendererMediaClock != null) {
+ throw ExoPlaybackException.createForUnexpected(
+ new IllegalStateException("Multiple renderer media clocks enabled."));
+ }
+ rendererMediaClock = mediaClock;
+ rendererMediaClockSource = renderer;
+ rendererMediaClock.setPlaybackParameters(playbackParameters);
+ }
+ // Start the renderer if playing.
+ if (playing) {
+ renderer.start();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Holds a {@link MediaPeriod} with information required to play it as part of a timeline.
+ */
+ private static final class MediaPeriodHolder {
+
+ public final MediaPeriod mediaPeriod;
+ public final Object uid;
+ public final SampleStream[] sampleStreams;
+ public final boolean[] mayRetainStreamFlags;
+ public final long rendererPositionOffsetUs;
+
+ public int index;
+ public long startPositionUs;
+ public boolean isLast;
+ public boolean prepared;
+ public boolean hasEnabledTracks;
+ public MediaPeriodHolder next;
+ public boolean needsContinueLoading;
+ public TrackSelectorResult trackSelectorResult;
+
+ private final Renderer[] renderers;
+ private final RendererCapabilities[] rendererCapabilities;
+ private final TrackSelector trackSelector;
+ private final LoadControl loadControl;
+ private final MediaSource mediaSource;
+
+ private TrackSelectorResult periodTrackSelectorResult;
+
+ public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
+ long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
+ MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod,
+ long startPositionUs) {
+ this.renderers = renderers;
+ this.rendererCapabilities = rendererCapabilities;
+ this.rendererPositionOffsetUs = rendererPositionOffsetUs;
+ this.trackSelector = trackSelector;
+ this.loadControl = loadControl;
+ this.mediaSource = mediaSource;
+ this.uid = Assertions.checkNotNull(periodUid);
+ this.index = periodIndex;
+ this.isLast = isLastPeriod;
+ this.startPositionUs = startPositionUs;
+ sampleStreams = new SampleStream[renderers.length];
+ mayRetainStreamFlags = new boolean[renderers.length];
+ mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(),
+ startPositionUs);
+ }
+
+ public long toRendererTime(long periodTimeUs) {
+ return periodTimeUs + getRendererOffset();
+ }
+
+ public long toPeriodTime(long rendererTimeUs) {
+ return rendererTimeUs - getRendererOffset();
+ }
+
+ public long getRendererOffset() {
+ return rendererPositionOffsetUs - startPositionUs;
+ }
+
+ public void setIndex(int index, boolean isLast) {
+ this.index = index;
+ this.isLast = isLast;
+ }
+
+ public boolean isFullyBuffered() {
+ return prepared
+ && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
+ }
+
+ public void handlePrepared() throws ExoPlaybackException {
+ prepared = true;
+ selectTracks();
+ startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
+ }
+
+ public boolean selectTracks() throws ExoPlaybackException {
+ TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
+ mediaPeriod.getTrackGroups());
+ if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
+ return false;
+ }
+ trackSelectorResult = selectorResult;
+ return true;
+ }
+
+ public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) {
+ return updatePeriodTrackSelection(positionUs, forceRecreateStreams,
+ new boolean[renderers.length]);
+ }
+
+ public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
+ boolean[] streamResetFlags) {
+ TrackSelectionArray trackSelections = trackSelectorResult.selections;
+ for (int i = 0; i < trackSelections.length; i++) {
+ mayRetainStreamFlags[i] = !forceRecreateStreams
+ && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
+ }
+
+ // Disable streams on the period and get new streams for updated/newly-enabled tracks.
+ positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
+ sampleStreams, streamResetFlags, positionUs);
+ periodTrackSelectorResult = trackSelectorResult;
+
+ // Update whether we have enabled tracks and sanity check the expected streams are non-null.
+ hasEnabledTracks = false;
+ for (int i = 0; i < sampleStreams.length; i++) {
+ if (sampleStreams[i] != null) {
+ Assertions.checkState(trackSelections.get(i) != null);
+ hasEnabledTracks = true;
+ } else {
+ Assertions.checkState(trackSelections.get(i) == null);
+ }
+ }
+
+ // The track selection has changed.
+ loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
+ return positionUs;
+ }
+
+ public void release() {
+ try {
+ mediaSource.releasePeriod(mediaPeriod);
+ } catch (RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Period release failed.", e);
+ }
+ }
+
+ }
+
+ private static final class SeekPosition {
+
+ public final Timeline timeline;
+ public final int windowIndex;
+ public final long windowPositionUs;
+
+ public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) {
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.windowPositionUs = windowPositionUs;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+/**
+ * Information about the ExoPlayer library.
+ */
+public interface ExoPlayerLibraryInfo {
+
+ /**
+ * The version of the library expressed as a string, for example "1.2.3".
+ */
+ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
+ String VERSION = "2.4.0";
+
+ /**
+ * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
+ */
+ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
+ String VERSION_SLASHY = "ExoPlayerLib/2.4.0";
+
+ /**
+ * The version of the library expressed as an integer, for example 1002003.
+ * <p>
+ * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
+ * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
+ * integer version 123045006 (123-045-006).
+ */
+ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
+ int VERSION_INT = 2004000;
+
+ /**
+ * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
+ * checks enabled.
+ */
+ boolean ASSERTIONS_ENABLED = true;
+
+ /**
+ * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}
+ * trace enabled.
+ */
+ boolean TRACE_ENABLED = true;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/Format.java
@@ -0,0 +1,723 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.MediaFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.ColorInfo;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Representation of a media format.
+ */
+public final class Format implements Parcelable {
+
+ /**
+ * A value for various fields to indicate that the field's value is unknown or not applicable.
+ */
+ public static final int NO_VALUE = -1;
+
+ /**
+ * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to
+ * the timestamps of their parent samples.
+ */
+ public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE;
+
+ /**
+ * An identifier for the format, or null if unknown or not applicable.
+ */
+ public final String id;
+ /**
+ * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int bitrate;
+ /**
+ * Codecs of the format as described in RFC 6381, or null if unknown or not applicable.
+ */
+ public final String codecs;
+ /**
+ * Metadata, or null if unknown or not applicable.
+ */
+ public final Metadata metadata;
+
+ // Container specific.
+
+ /**
+ * The mime type of the container, or null if unknown or not applicable.
+ */
+ public final String containerMimeType;
+
+ // Elementary stream specific.
+
+ /**
+ * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not
+ * applicable.
+ */
+ public final String sampleMimeType;
+ /**
+ * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or
+ * not applicable.
+ */
+ public final int maxInputSize;
+ /**
+ * Initialization data that must be provided to the decoder. Will not be null, but may be empty
+ * if initialization data is not required.
+ */
+ public final List<byte[]> initializationData;
+ /**
+ * DRM initialization data if the stream is protected, or null otherwise.
+ */
+ public final DrmInitData drmInitData;
+
+ // Video specific.
+
+ /**
+ * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int width;
+ /**
+ * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int height;
+ /**
+ * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final float frameRate;
+ /**
+ * The clockwise rotation that should be applied to the video for it to be rendered in the correct
+ * orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are
+ * supported.
+ */
+ public final int rotationDegrees;
+ /**
+ * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not
+ * applicable.
+ */
+ public final float pixelWidthHeightRatio;
+ /**
+ * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo
+ * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
+ * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.
+ */
+ @C.StereoMode
+ public final int stereoMode;
+ /**
+ * The projection data for 360/VR video, or null if not applicable.
+ */
+ public final byte[] projectionData;
+ /**
+ * The color metadata associated with the video, helps with accurate color reproduction.
+ */
+ public final ColorInfo colorInfo;
+
+ // Audio specific.
+
+ /**
+ * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int channelCount;
+ /**
+ * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
+ */
+ public final int sampleRate;
+ /**
+ * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}
+ * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT},
+ * {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for
+ * other media types.
+ */
+ @C.PcmEncoding
+ public final int pcmEncoding;
+ /**
+ * The number of samples to trim from the start of the decoded audio stream.
+ */
+ public final int encoderDelay;
+ /**
+ * The number of samples to trim from the end of the decoded audio stream.
+ */
+ public final int encoderPadding;
+
+ // Text specific.
+
+ /**
+ * For samples that contain subsamples, this is an offset that should be added to subsample
+ * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
+ * relative to the timestamps of their parent samples.
+ */
+ public final long subsampleOffsetUs;
+
+ // Audio and text specific.
+
+ /**
+ * Track selection flags.
+ */
+ @C.SelectionFlags
+ public final int selectionFlags;
+
+ /**
+ * The language, or null if unknown or not applicable.
+ */
+ public final String language;
+
+ /**
+ * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.
+ */
+ public final int accessibilityChannel;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ // Video.
+
+ public static Format createVideoContainerFormat(String id, String containerMimeType,
+ String sampleMimeType, String codecs, int bitrate, int width, int height,
+ float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
+ height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+ initializationData, null, null);
+ }
+
+ public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int width, int height, float frameRate,
+ List<byte[]> initializationData, DrmInitData drmInitData) {
+ return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData);
+ }
+
+ public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int width, int height, float frameRate,
+ List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
+ DrmInitData drmInitData) {
+ return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null,
+ NO_VALUE, null, drmInitData);
+ }
+
+ public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int width, int height, float frameRate,
+ List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
+ byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo,
+ DrmInitData drmInitData) {
+ return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
+ frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null);
+ }
+
+ // Audio.
+
+ public static Format createAudioContainerFormat(String id, String containerMimeType,
+ String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate,
+ List<byte[]> initializationData, @C.SelectionFlags int selectionFlags, String language) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate,
+ NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+ initializationData, null, null);
+ }
+
+ public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int channelCount, int sampleRate,
+ List<byte[]> initializationData, DrmInitData drmInitData,
+ @C.SelectionFlags int selectionFlags, String language) {
+ return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
+ sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language);
+ }
+
+ public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int channelCount, int sampleRate,
+ @C.PcmEncoding int pcmEncoding, List<byte[]> initializationData, DrmInitData drmInitData,
+ @C.SelectionFlags int selectionFlags, String language) {
+ return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
+ sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
+ selectionFlags, language, null);
+ }
+
+ public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int channelCount, int sampleRate,
+ @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
+ List<byte[]> initializationData, DrmInitData drmInitData,
+ @C.SelectionFlags int selectionFlags, String language, Metadata metadata) {
+ return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding,
+ encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+ initializationData, drmInitData, metadata);
+ }
+
+ // Text.
+
+ public static Format createTextContainerFormat(String id, String containerMimeType,
+ String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+ String language) {
+ return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
+ selectionFlags, language, NO_VALUE);
+ }
+
+ public static Format createTextContainerFormat(String id, String containerMimeType,
+ String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+ String language, int accessibilityChannel) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel,
+ OFFSET_SAMPLE_RELATIVE, null, null, null);
+ }
+
+ public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
+ return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+ NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.<byte[]>emptyList());
+ }
+
+ public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
+ DrmInitData drmInitData) {
+ return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+ accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.<byte[]>emptyList());
+ }
+
+ public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
+ long subsampleOffsetUs) {
+ return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+ NO_VALUE, drmInitData, subsampleOffsetUs, Collections.<byte[]>emptyList());
+ }
+
+ public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, @C.SelectionFlags int selectionFlags, String language,
+ int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs,
+ List<byte[]> initializationData) {
+ return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, null);
+ }
+
+ // Image.
+
+ public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) {
+ return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData,
+ null);
+ }
+
+ // Generic.
+
+ public static Format createContainerFormat(String id, String containerMimeType,
+ String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+ String language) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
+ null);
+ }
+
+ public static Format createSampleFormat(String id, String sampleMimeType,
+ long subsampleOffsetUs) {
+ return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
+ }
+
+ public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
+ int bitrate, DrmInitData drmInitData) {
+ return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+ NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
+ }
+
+ /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
+ int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees,
+ float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
+ ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding,
+ int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
+ int accessibilityChannel, long subsampleOffsetUs, List<byte[]> initializationData,
+ DrmInitData drmInitData, Metadata metadata) {
+ this.id = id;
+ this.containerMimeType = containerMimeType;
+ this.sampleMimeType = sampleMimeType;
+ this.codecs = codecs;
+ this.bitrate = bitrate;
+ this.maxInputSize = maxInputSize;
+ this.width = width;
+ this.height = height;
+ this.frameRate = frameRate;
+ this.rotationDegrees = rotationDegrees;
+ this.pixelWidthHeightRatio = pixelWidthHeightRatio;
+ this.projectionData = projectionData;
+ this.stereoMode = stereoMode;
+ this.colorInfo = colorInfo;
+ this.channelCount = channelCount;
+ this.sampleRate = sampleRate;
+ this.pcmEncoding = pcmEncoding;
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ this.selectionFlags = selectionFlags;
+ this.language = language;
+ this.accessibilityChannel = accessibilityChannel;
+ this.subsampleOffsetUs = subsampleOffsetUs;
+ this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
+ : initializationData;
+ this.drmInitData = drmInitData;
+ this.metadata = metadata;
+ }
+
+ @SuppressWarnings("ResourceType")
+ /* package */ Format(Parcel in) {
+ id = in.readString();
+ containerMimeType = in.readString();
+ sampleMimeType = in.readString();
+ codecs = in.readString();
+ bitrate = in.readInt();
+ maxInputSize = in.readInt();
+ width = in.readInt();
+ height = in.readInt();
+ frameRate = in.readFloat();
+ rotationDegrees = in.readInt();
+ pixelWidthHeightRatio = in.readFloat();
+ boolean hasProjectionData = in.readInt() != 0;
+ projectionData = hasProjectionData ? in.createByteArray() : null;
+ stereoMode = in.readInt();
+ colorInfo = in.readParcelable(ColorInfo.class.getClassLoader());
+ channelCount = in.readInt();
+ sampleRate = in.readInt();
+ pcmEncoding = in.readInt();
+ encoderDelay = in.readInt();
+ encoderPadding = in.readInt();
+ selectionFlags = in.readInt();
+ language = in.readString();
+ accessibilityChannel = in.readInt();
+ subsampleOffsetUs = in.readLong();
+ int initializationDataSize = in.readInt();
+ initializationData = new ArrayList<>(initializationDataSize);
+ for (int i = 0; i < initializationDataSize; i++) {
+ initializationData.add(in.createByteArray());
+ }
+ drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
+ metadata = in.readParcelable(Metadata.class.getClassLoader());
+ }
+
+ public Format copyWithMaxInputSize(int maxInputSize) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+ width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+ stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+ encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, metadata);
+ }
+
+ public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+ width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+ stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+ encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, metadata);
+ }
+
+ public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
+ @C.SelectionFlags int selectionFlags, String language) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+ width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+ stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+ encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, metadata);
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ public Format copyWithManifestFormatInfo(Format manifestFormat) {
+ if (this == manifestFormat) {
+ // No need to copy from ourselves.
+ return this;
+ }
+ String id = manifestFormat.id;
+ String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
+ int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
+ float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
+ @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
+ String language = this.language == null ? manifestFormat.language : this.language;
+ DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData
+ : this.drmInitData;
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+ height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+ colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+ selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+ drmInitData, metadata);
+ }
+
+ public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+ width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+ stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+ encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, metadata);
+ }
+
+ public Format copyWithDrmInitData(DrmInitData drmInitData) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+ width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+ stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+ encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, metadata);
+ }
+
+ public Format copyWithMetadata(Metadata metadata) {
+ return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+ width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+ stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+ encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+ initializationData, drmInitData, metadata);
+ }
+
+ /**
+ * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
+ * are known, or {@link #NO_VALUE} otherwise
+ */
+ public int getPixelCount() {
+ return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height);
+ }
+
+ /**
+ * Returns a {@link MediaFormat} representation of this format.
+ */
+ @SuppressLint("InlinedApi")
+ @TargetApi(16)
+ public final MediaFormat getFrameworkMediaFormatV16() {
+ MediaFormat format = new MediaFormat();
+ format.setString(MediaFormat.KEY_MIME, sampleMimeType);
+ maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
+ maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+ maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
+ maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
+ maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
+ maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
+ maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
+ maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
+ maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
+ maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
+ for (int i = 0; i < initializationData.size(); i++) {
+ format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
+ }
+ maybeSetColorInfoV24(format, colorInfo);
+ return format;
+ }
+
+ @Override
+ public String toString() {
+ return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
+ + language + ", [" + width + ", " + height + ", " + frameRate + "]"
+ + ", [" + channelCount + ", " + sampleRate + "])";
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + (id == null ? 0 : id.hashCode());
+ result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode());
+ result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode());
+ result = 31 * result + (codecs == null ? 0 : codecs.hashCode());
+ result = 31 * result + bitrate;
+ result = 31 * result + width;
+ result = 31 * result + height;
+ result = 31 * result + channelCount;
+ result = 31 * result + sampleRate;
+ result = 31 * result + (language == null ? 0 : language.hashCode());
+ result = 31 * result + accessibilityChannel;
+ result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
+ result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Format other = (Format) obj;
+ if (bitrate != other.bitrate || maxInputSize != other.maxInputSize
+ || width != other.width || height != other.height || frameRate != other.frameRate
+ || rotationDegrees != other.rotationDegrees
+ || pixelWidthHeightRatio != other.pixelWidthHeightRatio || stereoMode != other.stereoMode
+ || channelCount != other.channelCount || sampleRate != other.sampleRate
+ || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay
+ || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs
+ || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id)
+ || !Util.areEqual(language, other.language)
+ || accessibilityChannel != other.accessibilityChannel
+ || !Util.areEqual(containerMimeType, other.containerMimeType)
+ || !Util.areEqual(sampleMimeType, other.sampleMimeType)
+ || !Util.areEqual(codecs, other.codecs)
+ || !Util.areEqual(drmInitData, other.drmInitData)
+ || !Util.areEqual(metadata, other.metadata)
+ || !Util.areEqual(colorInfo, other.colorInfo)
+ || !Arrays.equals(projectionData, other.projectionData)
+ || initializationData.size() != other.initializationData.size()) {
+ return false;
+ }
+ for (int i = 0; i < initializationData.size(); i++) {
+ if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @TargetApi(24)
+ private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) {
+ if (colorInfo == null) {
+ return;
+ }
+ maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer);
+ maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace);
+ maybeSetIntegerV16(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange);
+ maybeSetByteBufferV16(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo);
+ }
+
+ @TargetApi(16)
+ private static void maybeSetStringV16(MediaFormat format, String key, String value) {
+ if (value != null) {
+ format.setString(key, value);
+ }
+ }
+
+ @TargetApi(16)
+ private static void maybeSetIntegerV16(MediaFormat format, String key, int value) {
+ if (value != NO_VALUE) {
+ format.setInteger(key, value);
+ }
+ }
+
+ @TargetApi(16)
+ private static void maybeSetFloatV16(MediaFormat format, String key, float value) {
+ if (value != NO_VALUE) {
+ format.setFloat(key, value);
+ }
+ }
+
+ @TargetApi(16)
+ private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) {
+ if (value != null) {
+ format.setByteBuffer(key, ByteBuffer.wrap(value));
+ }
+ }
+
+ // Utility methods
+
+ /**
+ * Returns a prettier {@link String} than {@link #toString()}, intended for logging.
+ */
+ public static String toLogString(Format format) {
+ if (format == null) {
+ return "null";
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
+ if (format.bitrate != Format.NO_VALUE) {
+ builder.append(", bitrate=").append(format.bitrate);
+ }
+ if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
+ builder.append(", res=").append(format.width).append("x").append(format.height);
+ }
+ if (format.frameRate != Format.NO_VALUE) {
+ builder.append(", fps=").append(format.frameRate);
+ }
+ if (format.channelCount != Format.NO_VALUE) {
+ builder.append(", channels=").append(format.channelCount);
+ }
+ if (format.sampleRate != Format.NO_VALUE) {
+ builder.append(", sample_rate=").append(format.sampleRate);
+ }
+ if (format.language != null) {
+ builder.append(", language=").append(format.language);
+ }
+ return builder.toString();
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(containerMimeType);
+ dest.writeString(sampleMimeType);
+ dest.writeString(codecs);
+ dest.writeInt(bitrate);
+ dest.writeInt(maxInputSize);
+ dest.writeInt(width);
+ dest.writeInt(height);
+ dest.writeFloat(frameRate);
+ dest.writeInt(rotationDegrees);
+ dest.writeFloat(pixelWidthHeightRatio);
+ dest.writeInt(projectionData != null ? 1 : 0);
+ if (projectionData != null) {
+ dest.writeByteArray(projectionData);
+ }
+ dest.writeInt(stereoMode);
+ dest.writeParcelable(colorInfo, flags);
+ dest.writeInt(channelCount);
+ dest.writeInt(sampleRate);
+ dest.writeInt(pcmEncoding);
+ dest.writeInt(encoderDelay);
+ dest.writeInt(encoderPadding);
+ dest.writeInt(selectionFlags);
+ dest.writeString(language);
+ dest.writeInt(accessibilityChannel);
+ dest.writeLong(subsampleOffsetUs);
+ int initializationDataSize = initializationData.size();
+ dest.writeInt(initializationDataSize);
+ for (int i = 0; i < initializationDataSize; i++) {
+ dest.writeByteArray(initializationData.get(i));
+ }
+ dest.writeParcelable(drmInitData, 0);
+ dest.writeParcelable(metadata, 0);
+ }
+
+ public static final Creator<Format> CREATOR = new Creator<Format>() {
+
+ @Override
+ public Format createFromParcel(Parcel in) {
+ return new Format(in);
+ }
+
+ @Override
+ public Format[] newArray(int size) {
+ return new Format[size];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/FormatHolder.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+/**
+ * Holds a {@link Format}.
+ */
+public final class FormatHolder {
+
+ /**
+ * The held {@link Format}.
+ */
+ public Format format;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/IllegalSeekPositionException.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+/**
+ * Thrown when an attempt is made to seek to a position that does not exist in the player's
+ * {@link Timeline}.
+ */
+public final class IllegalSeekPositionException extends IllegalStateException {
+
+ /**
+ * The {@link Timeline} in which the seek was attempted.
+ */
+ public final Timeline timeline;
+ /**
+ * The index of the window being seeked to.
+ */
+ public final int windowIndex;
+ /**
+ * The seek position in the specified window.
+ */
+ public final long positionMs;
+
+ /**
+ * @param timeline The {@link Timeline} in which the seek was attempted.
+ * @param windowIndex The index of the window being seeked to.
+ * @param positionMs The seek position in the specified window.
+ */
+ public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) {
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.positionMs = positionMs;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/LoadControl.java
@@ -0,0 +1,78 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+
+/**
+ * Controls buffering of media.
+ */
+public interface LoadControl {
+
+ /**
+ * Called by the player when prepared with a new source.
+ */
+ void onPrepared();
+
+ /**
+ * Called by the player when a track selection occurs.
+ *
+ * @param renderers The renderers.
+ * @param trackGroups The {@link TrackGroup}s from which the selection was made.
+ * @param trackSelections The track selections that were made.
+ */
+ void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+ TrackSelectionArray trackSelections);
+
+ /**
+ * Called by the player when stopped.
+ */
+ void onStopped();
+
+ /**
+ * Called by the player when released.
+ */
+ void onReleased();
+
+ /**
+ * Returns the {@link Allocator} that should be used to obtain media buffer allocations.
+ */
+ Allocator getAllocator();
+
+ /**
+ * Called by the player to determine whether sufficient media is buffered for playback to be
+ * started or resumed.
+ *
+ * @param bufferedDurationUs The duration of media that's currently buffered.
+ * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
+ * buffer depletion rather than a user action. Hence this parameter is false during initial
+ * buffering and when buffering as a result of a seek operation.
+ * @return Whether playback should be allowed to start or resume.
+ */
+ boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering);
+
+ /**
+ * Called by the player to determine whether it should continue to load the source.
+ *
+ * @param bufferedDurationUs The duration of media that's currently buffered.
+ * @return Whether the loading should continue.
+ */
+ boolean shouldContinueLoading(long bufferedDurationUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ParserException.java
@@ -0,0 +1,51 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import java.io.IOException;
+
+/**
+ * Thrown when an error occurs parsing media data and metadata.
+ */
+public class ParserException extends IOException {
+
+ public ParserException() {
+ super();
+ }
+
+ /**
+ * @param message The detail message for the exception.
+ */
+ public ParserException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param cause The cause for the exception.
+ */
+ public ParserException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * @param message The detail message for the exception.
+ * @param cause The cause for the exception.
+ */
+ public ParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+/**
+ * The parameters that apply to playback.
+ */
+public final class PlaybackParameters {
+
+ /**
+ * The default playback parameters: real-time playback with no pitch modification.
+ */
+ public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f);
+
+ /**
+ * The factor by which playback will be sped up.
+ */
+ public final float speed;
+
+ /**
+ * The factor by which the audio pitch will be scaled.
+ */
+ public final float pitch;
+
+ private final int scaledUsPerMs;
+
+ /**
+ * Creates new playback parameters.
+ *
+ * @param speed The factor by which playback will be sped up.
+ * @param pitch The factor by which the audio pitch will be scaled.
+ */
+ public PlaybackParameters(float speed, float pitch) {
+ this.speed = speed;
+ this.pitch = pitch;
+ scaledUsPerMs = Math.round(speed * 1000f);
+ }
+
+ /**
+ * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in
+ * microseconds.
+ *
+ * @param timeMs The time to scale, in milliseconds.
+ * @return The scaled time, in microseconds.
+ */
+ public long getSpeedAdjustedDurationUs(long timeMs) {
+ return timeMs * scaledUsPerMs;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PlaybackParameters other = (PlaybackParameters) obj;
+ return this.speed == other.speed && this.pitch == other.pitch;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Float.floatToRawIntBits(speed);
+ result = 31 * result + Float.floatToRawIntBits(pitch);
+ return result;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/Renderer.java
@@ -0,0 +1,257 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.MediaClock;
+import java.io.IOException;
+
+/**
+ * Renders media read from a {@link SampleStream}.
+ * <p>
+ * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is
+ * transitioned through various states as the overall playback state changes. The valid state
+ * transitions are shown below, annotated with the methods that are called during each transition.
+ * <p align="center">
+ * <img src="doc-files/renderer-states.svg" alt="Renderer state transitions">
+ * </p>
+ */
+public interface Renderer extends ExoPlayerComponent {
+
+ /**
+ * The renderer is disabled.
+ */
+ int STATE_DISABLED = 0;
+ /**
+ * The renderer is enabled but not started. A renderer in this state is not actively rendering
+ * media, but will typically hold resources that it requires for rendering (e.g. media decoders).
+ */
+ int STATE_ENABLED = 1;
+ /**
+ * The renderer is started. Calls to {@link #render(long, long)} will cause media to be rendered.
+ */
+ int STATE_STARTED = 2;
+
+ /**
+ * Returns the track type that the {@link Renderer} handles. For example, a video renderer will
+ * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
+ * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
+ *
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ int getTrackType();
+
+ /**
+ * Returns the capabilities of the renderer.
+ *
+ * @return The capabilities of the renderer.
+ */
+ RendererCapabilities getCapabilities();
+
+ /**
+ * Sets the index of this renderer within the player.
+ *
+ * @param index The renderer index.
+ */
+ void setIndex(int index);
+
+ /**
+ * If the renderer advances its own playback position then this method returns a corresponding
+ * {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its
+ * source of time during playback. A player may have at most one renderer that returns a
+ * {@link MediaClock} from this method.
+ *
+ * @return The {@link MediaClock} tracking the playback position of the renderer, or null.
+ */
+ MediaClock getMediaClock();
+
+ /**
+ * Returns the current state of the renderer.
+ *
+ * @return The current state (one of the {@code STATE_*} constants).
+ */
+ int getState();
+
+ /**
+ * Enables the renderer to consume from the specified {@link SampleStream}.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_DISABLED}.
+ *
+ * @param configuration The renderer configuration.
+ * @param formats The enabled formats.
+ * @param stream The {@link SampleStream} from which the renderer should consume.
+ * @param positionUs The player's current position.
+ * @param joining Whether this renderer is being enabled to join an ongoing playback.
+ * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream}
+ * before they are rendered.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
+ long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
+
+ /**
+ * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
+ * rendered.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void start() throws ExoPlaybackException;
+
+ /**
+ * Replaces the {@link SampleStream} from which samples will be consumed.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @param formats The enabled formats.
+ * @param stream The {@link SampleStream} from which the renderer should consume.
+ * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before
+ * they are rendered.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
+ throws ExoPlaybackException;
+
+ /**
+ * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled.
+ */
+ SampleStream getStream();
+
+ /**
+ * Returns whether the renderer has read the current {@link SampleStream} to the end.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ */
+ boolean hasReadStreamToEnd();
+
+ /**
+ * Signals to the renderer that the current {@link SampleStream} will be the final one supplied
+ * before it is next disabled or reset.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ */
+ void setCurrentStreamFinal();
+
+ /**
+ * Returns whether the current {@link SampleStream} will be the final one supplied before the
+ * renderer is next disabled or reset.
+ */
+ boolean isCurrentStreamFinal();
+
+ /**
+ * Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does
+ * nothing if no such error exists.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @throws IOException An error that's preventing the renderer from making progress or buffering
+ * more data.
+ */
+ void maybeThrowStreamError() throws IOException;
+
+ /**
+ * Signals to the renderer that a position discontinuity has occurred.
+ * <p>
+ * After a position discontinuity, the renderer's {@link SampleStream} is guaranteed to provide
+ * samples starting from a key frame.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @param positionUs The new playback position in microseconds.
+ * @throws ExoPlaybackException If an error occurs handling the reset.
+ */
+ void resetPosition(long positionUs) throws ExoPlaybackException;
+
+ /**
+ * Incrementally renders the {@link SampleStream}.
+ * <p>
+ * If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do
+ * work toward being ready to render the {@link SampleStream} when the renderer is started. It may
+ * also render the very start of the media, for example the first frame of a video stream. If the
+ * renderer is in the {@link #STATE_STARTED} state then calls to this method will render the
+ * {@link SampleStream} in sync with the specified media positions.
+ * <p>
+ * This method should return quickly, and should not block if the renderer is unable to make
+ * useful progress.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @param positionUs The current media time in microseconds, measured at the start of the
+ * current iteration of the rendering loop.
+ * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
+ * measured at the start of the current iteration of the rendering loop.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException;
+
+ /**
+ * Whether the renderer is able to immediately render media from the current position.
+ * <p>
+ * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
+ * renderer has everything that it needs to continue playback. Returning false indicates that
+ * the player should pause until the renderer is ready.
+ * <p>
+ * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
+ * renderer is ready for playback to be started. Returning false indicates that it is not.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @return Whether the renderer is ready to render media.
+ */
+ boolean isReady();
+
+ /**
+ * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
+ * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is
+ * returned by all of its {@link Renderer}s.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+ *
+ * @return Whether the renderer is ready for the player to transition to the ended state.
+ */
+ boolean isEnded();
+
+ /**
+ * Stops the renderer, transitioning it to the {@link #STATE_ENABLED} state.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_STARTED}.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ void stop() throws ExoPlaybackException;
+
+ /**
+ * Disable the renderer, transitioning it to the {@link #STATE_DISABLED} state.
+ * <p>
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}.
+ */
+ void disable();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/RendererCapabilities.java
@@ -0,0 +1,140 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * Defines the capabilities of a {@link Renderer}.
+ */
+public interface RendererCapabilities {
+
+ /**
+ * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+ * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES},
+ * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
+ */
+ int FORMAT_SUPPORT_MASK = 0b11;
+ /**
+ * The {@link Renderer} is capable of rendering the format.
+ */
+ int FORMAT_HANDLED = 0b11;
+ /**
+ * The {@link Renderer} is capable of rendering formats with the same mime type, but the
+ * properties of the format exceed the renderer's capability.
+ * <p>
+ * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is
+ * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported
+ * by the underlying H264 decoder.
+ */
+ int FORMAT_EXCEEDS_CAPABILITIES = 0b10;
+ /**
+ * The {@link Renderer} is a general purpose renderer for formats of the same top-level type,
+ * but is not capable of rendering the format or any other format with the same mime type because
+ * the sub-type is not supported.
+ * <p>
+ * Example: The {@link Renderer} is a general purpose audio renderer and the format's
+ * mime type matches audio/[subtype], but there does not exist a suitable decoder for [subtype].
+ */
+ int FORMAT_UNSUPPORTED_SUBTYPE = 0b01;
+ /**
+ * The {@link Renderer} is not capable of rendering the format, either because it does not
+ * support the format's top-level type, or because it's a specialized renderer for a different
+ * mime type.
+ * <p>
+ * Example: The {@link Renderer} is a general purpose video renderer, but the format has an
+ * audio mime type.
+ */
+ int FORMAT_UNSUPPORTED_TYPE = 0b00;
+
+ /**
+ * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+ * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}.
+ */
+ int ADAPTIVE_SUPPORT_MASK = 0b1100;
+ /**
+ * The {@link Renderer} can seamlessly adapt between formats.
+ */
+ int ADAPTIVE_SEAMLESS = 0b1000;
+ /**
+ * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
+ * (~50-100ms) when adaptation occurs.
+ */
+ int ADAPTIVE_NOT_SEAMLESS = 0b0100;
+ /**
+ * The {@link Renderer} does not support adaptation between formats.
+ */
+ int ADAPTIVE_NOT_SUPPORTED = 0b0000;
+
+ /**
+ * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+ * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
+ */
+ int TUNNELING_SUPPORT_MASK = 0b10000;
+ /**
+ * The {@link Renderer} supports tunneled output.
+ */
+ int TUNNELING_SUPPORTED = 0b10000;
+ /**
+ * The {@link Renderer} does not support tunneled output.
+ */
+ int TUNNELING_NOT_SUPPORTED = 0b00000;
+
+ /**
+ * Returns the track type that the {@link Renderer} handles. For example, a video renderer will
+ * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
+ * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
+ *
+ * @see Renderer#getTrackType()
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ int getTrackType();
+
+ /**
+ * Returns the extent to which the {@link Renderer} supports a given format. The returned value is
+ * the bitwise OR of three properties:
+ * <ul>
+ * <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
+ * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
+ * {@link #FORMAT_UNSUPPORTED_TYPE}.</li>
+ * <li>The level of support for adapting from the format to another format of the same mime type.
+ * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
+ * {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
+ * <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
+ * {@link #TUNNELING_NOT_SUPPORTED}.</li>
+ * </ul>
+ * The individual properties can be retrieved by performing a bitwise AND with
+ * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and
+ * {@link #TUNNELING_SUPPORT_MASK} respectively.
+ *
+ * @param format The format.
+ * @return The extent to which the renderer is capable of supporting the given format.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ int supportsFormat(Format format) throws ExoPlaybackException;
+
+ /**
+ * Returns the extent to which the {@link Renderer} supports adapting between supported formats
+ * that have different mime types.
+ *
+ * @return The extent to which the renderer supports adapting between supported formats that have
+ * different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
+ * {@link #ADAPTIVE_NOT_SUPPORTED}.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/RendererConfiguration.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+/**
+ * The configuration of a {@link Renderer}.
+ */
+public final class RendererConfiguration {
+
+ /**
+ * The default configuration.
+ */
+ public static final RendererConfiguration DEFAULT =
+ new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET);
+
+ /**
+ * The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
+ * should not be enabled.
+ */
+ public final int tunnelingAudioSessionId;
+
+ /**
+ * @param tunnelingAudioSessionId The audio session id to use for tunneling, or
+ * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
+ */
+ public RendererConfiguration(int tunnelingAudioSessionId) {
+ this.tunnelingAudioSessionId = tunnelingAudioSessionId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ RendererConfiguration other = (RendererConfiguration) obj;
+ return tunnelingAudioSessionId == other.tunnelingAudioSessionId;
+ }
+
+ @Override
+ public int hashCode() {
+ return tunnelingAudioSessionId;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/RenderersFactory.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 com.google.android.exoplayer2;
+
+import android.os.Handler;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+
+/**
+ * Builds {@link Renderer} instances for use by a {@link SimpleExoPlayer}.
+ */
+public interface RenderersFactory {
+
+ /**
+ * Builds the {@link Renderer} instances for a {@link SimpleExoPlayer}.
+ *
+ * @param eventHandler A handler to use when invoking event listeners and outputs.
+ * @param videoRendererEventListener An event listener for video renderers.
+ * @param videoRendererEventListener An event listener for audio renderers.
+ * @param textRendererOutput An output for text renderers.
+ * @param metadataRendererOutput An output for metadata renderers.
+ * @return The {@link Renderer instances}.
+ */
+ Renderer[] createRenderers(Handler eventHandler,
+ VideoRendererEventListener videoRendererEventListener,
+ AudioRendererEventListener audioRendererEventListener,
+ TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -0,0 +1,870 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.media.MediaCodec;
+import android.media.PlaybackParams;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.util.List;
+
+/**
+ * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
+ * be obtained from {@link ExoPlayerFactory}.
+ */
+@TargetApi(16)
+public class SimpleExoPlayer implements ExoPlayer {
+
+ /**
+ * A listener for video rendering information from a {@link SimpleExoPlayer}.
+ */
+ public interface VideoListener {
+
+ /**
+ * Called each time there's a change in the size of the video being rendered.
+ *
+ * @param width The video width in pixels.
+ * @param height The video height in pixels.
+ * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
+ * rotation in degrees that the application should apply for the video for it to be rendered
+ * in the correct orientation. This value will always be zero on API levels 21 and above,
+ * since the renderer will apply all necessary rotations internally. On earlier API levels
+ * this is not possible. Applications that use {@link android.view.TextureView} can apply
+ * the rotation by calling {@link android.view.TextureView#setTransform}. Applications that
+ * do not expect to encounter rotated videos can safely ignore this parameter.
+ * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
+ * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
+ * content.
+ */
+ void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+ float pixelWidthHeightRatio);
+
+ /**
+ * Called when a frame is rendered for the first time since setting the surface, and when a
+ * frame is rendered for the first time since a video track was selected.
+ */
+ void onRenderedFirstFrame();
+
+ }
+
+ private static final String TAG = "SimpleExoPlayer";
+
+ protected final Renderer[] renderers;
+
+ private final ExoPlayer player;
+ private final ComponentListener componentListener;
+ private final int videoRendererCount;
+ private final int audioRendererCount;
+
+ private Format videoFormat;
+ private Format audioFormat;
+
+ private Surface surface;
+ private boolean ownsSurface;
+ @C.VideoScalingMode
+ private int videoScalingMode;
+ private SurfaceHolder surfaceHolder;
+ private TextureView textureView;
+ private TextRenderer.Output textOutput;
+ private MetadataRenderer.Output metadataOutput;
+ private VideoListener videoListener;
+ private AudioRendererEventListener audioDebugListener;
+ private VideoRendererEventListener videoDebugListener;
+ private DecoderCounters videoDecoderCounters;
+ private DecoderCounters audioDecoderCounters;
+ private int audioSessionId;
+ @C.StreamType
+ private int audioStreamType;
+ private float audioVolume;
+
+ protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
+ LoadControl loadControl) {
+ componentListener = new ComponentListener();
+ renderers = renderersFactory.createRenderers(new Handler(), componentListener,
+ componentListener, componentListener, componentListener);
+
+ // Obtain counts of video and audio renderers.
+ int videoRendererCount = 0;
+ int audioRendererCount = 0;
+ for (Renderer renderer : renderers) {
+ switch (renderer.getTrackType()) {
+ case C.TRACK_TYPE_VIDEO:
+ videoRendererCount++;
+ break;
+ case C.TRACK_TYPE_AUDIO:
+ audioRendererCount++;
+ break;
+ }
+ }
+ this.videoRendererCount = videoRendererCount;
+ this.audioRendererCount = audioRendererCount;
+
+ // Set initial values.
+ audioVolume = 1;
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ audioStreamType = C.STREAM_TYPE_DEFAULT;
+ videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+
+ // Build the player and associated objects.
+ player = new ExoPlayerImpl(renderers, trackSelector, loadControl);
+ }
+
+ /**
+ * Sets the video scaling mode.
+ * <p>
+ * Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is
+ * enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
+ *
+ * @param videoScalingMode The video scaling mode.
+ */
+ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
+ this.videoScalingMode = videoScalingMode;
+ ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
+ int count = 0;
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
+ messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE,
+ videoScalingMode);
+ }
+ }
+ player.sendMessages(messages);
+ }
+
+ /**
+ * Returns the video scaling mode.
+ */
+ public @C.VideoScalingMode int getVideoScalingMode() {
+ return videoScalingMode;
+ }
+
+ /**
+ * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
+ * currently set on the player.
+ */
+ public void clearVideoSurface() {
+ setVideoSurface(null);
+ }
+
+ /**
+ * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
+ * tracking the lifecycle of the surface, and must clear the surface by calling
+ * {@code setVideoSurface(null)} if the surface is destroyed.
+ * <p>
+ * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link SurfaceHolder}
+ * then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)},
+ * {@link #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)}
+ * rather than this method, since passing the holder allows the player to track the lifecycle of
+ * the surface automatically.
+ *
+ * @param surface The {@link Surface}.
+ */
+ public void setVideoSurface(Surface surface) {
+ removeSurfaceCallbacks();
+ setVideoSurfaceInternal(surface, false);
+ }
+
+ /**
+ * Clears the {@link Surface} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param surface The surface to clear.
+ */
+ public void clearVideoSurface(Surface surface) {
+ if (surface != null && surface == this.surface) {
+ setVideoSurface(null);
+ }
+ }
+
+ /**
+ * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be
+ * rendered. The player will track the lifecycle of the surface automatically.
+ *
+ * @param surfaceHolder The surface holder.
+ */
+ public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
+ removeSurfaceCallbacks();
+ this.surfaceHolder = surfaceHolder;
+ if (surfaceHolder == null) {
+ setVideoSurfaceInternal(null, false);
+ } else {
+ setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
+ surfaceHolder.addCallback(componentListener);
+ }
+ }
+
+ /**
+ * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being
+ * rendered if it matches the one passed. Else does nothing.
+ *
+ * @param surfaceHolder The surface holder to clear.
+ */
+ public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
+ if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {
+ setVideoSurfaceHolder(null);
+ }
+ }
+
+ /**
+ * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the
+ * lifecycle of the surface automatically.
+ *
+ * @param surfaceView The surface view.
+ */
+ public void setVideoSurfaceView(SurfaceView surfaceView) {
+ setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
+ }
+
+ /**
+ * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param surfaceView The texture view to clear.
+ */
+ public void clearVideoSurfaceView(SurfaceView surfaceView) {
+ clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
+ }
+
+ /**
+ * Sets the {@link TextureView} onto which video will be rendered. The player will track the
+ * lifecycle of the surface automatically.
+ *
+ * @param textureView The texture view.
+ */
+ public void setVideoTextureView(TextureView textureView) {
+ removeSurfaceCallbacks();
+ this.textureView = textureView;
+ if (textureView == null) {
+ setVideoSurfaceInternal(null, true);
+ } else {
+ if (textureView.getSurfaceTextureListener() != null) {
+ Log.w(TAG, "Replacing existing SurfaceTextureListener.");
+ }
+ SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
+ setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
+ textureView.setSurfaceTextureListener(componentListener);
+ }
+ }
+
+ /**
+ * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param textureView The texture view to clear.
+ */
+ public void clearVideoTextureView(TextureView textureView) {
+ if (textureView != null && textureView == this.textureView) {
+ setVideoTextureView(null);
+ }
+ }
+
+ /**
+ * Sets the stream type for audio playback (see {@link C.StreamType} and
+ * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type
+ * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}.
+ * <p>
+ * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
+ * introduce a brief gap in audio output. Note also that tracks in the same audio session must
+ * share the same routing, so a new audio session id will be generated.
+ *
+ * @param audioStreamType The stream type for audio playback.
+ */
+ public void setAudioStreamType(@C.StreamType int audioStreamType) {
+ this.audioStreamType = audioStreamType;
+ ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
+ int count = 0;
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+ messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType);
+ }
+ }
+ player.sendMessages(messages);
+ }
+
+ /**
+ * Returns the stream type for audio playback.
+ */
+ public @C.StreamType int getAudioStreamType() {
+ return audioStreamType;
+ }
+
+ /**
+ * Sets the audio volume, with 0 being silence and 1 being unity gain.
+ *
+ * @param audioVolume The audio volume.
+ */
+ public void setVolume(float audioVolume) {
+ this.audioVolume = audioVolume;
+ ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
+ int count = 0;
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+ messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume);
+ }
+ }
+ player.sendMessages(messages);
+ }
+
+ /**
+ * Returns the audio volume, with 0 being silence and 1 being unity gain.
+ */
+ public float getVolume() {
+ return audioVolume;
+ }
+
+ /**
+ * Sets the {@link PlaybackParams} governing audio playback.
+ *
+ * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}.
+ * @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
+ */
+ @Deprecated
+ @TargetApi(23)
+ public void setPlaybackParams(@Nullable PlaybackParams params) {
+ PlaybackParameters playbackParameters;
+ if (params != null) {
+ params.allowDefaults();
+ playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch());
+ } else {
+ playbackParameters = null;
+ }
+ setPlaybackParameters(playbackParameters);
+ }
+
+ /**
+ * Returns the video format currently being played, or null if no video is being played.
+ */
+ public Format getVideoFormat() {
+ return videoFormat;
+ }
+
+ /**
+ * Returns the audio format currently being played, or null if no audio is being played.
+ */
+ public Format getAudioFormat() {
+ return audioFormat;
+ }
+
+ /**
+ * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set.
+ */
+ public int getAudioSessionId() {
+ return audioSessionId;
+ }
+
+ /**
+ * Returns {@link DecoderCounters} for video, or null if no video is being played.
+ */
+ public DecoderCounters getVideoDecoderCounters() {
+ return videoDecoderCounters;
+ }
+
+ /**
+ * Returns {@link DecoderCounters} for audio, or null if no audio is being played.
+ */
+ public DecoderCounters getAudioDecoderCounters() {
+ return audioDecoderCounters;
+ }
+
+ /**
+ * Sets a listener to receive video events.
+ *
+ * @param listener The listener.
+ */
+ public void setVideoListener(VideoListener listener) {
+ videoListener = listener;
+ }
+
+ /**
+ * Clears the listener receiving video events if it matches the one passed. Else does nothing.
+ *
+ * @param listener The listener to clear.
+ */
+ public void clearVideoListener(VideoListener listener) {
+ if (videoListener == listener) {
+ videoListener = null;
+ }
+ }
+
+ /**
+ * Sets an output to receive text events.
+ *
+ * @param output The output.
+ */
+ public void setTextOutput(TextRenderer.Output output) {
+ textOutput = output;
+ }
+
+ /**
+ * Clears the output receiving text events if it matches the one passed. Else does nothing.
+ *
+ * @param output The output to clear.
+ */
+ public void clearTextOutput(TextRenderer.Output output) {
+ if (textOutput == output) {
+ textOutput = null;
+ }
+ }
+
+ /**
+ * Sets a listener to receive metadata events.
+ *
+ * @param output The output.
+ */
+ public void setMetadataOutput(MetadataRenderer.Output output) {
+ metadataOutput = output;
+ }
+
+ /**
+ * Clears the output receiving metadata events if it matches the one passed. Else does nothing.
+ *
+ * @param output The output to clear.
+ */
+ public void clearMetadataOutput(MetadataRenderer.Output output) {
+ if (metadataOutput == output) {
+ metadataOutput = null;
+ }
+ }
+
+ /**
+ * Sets a listener to receive debug events from the video renderer.
+ *
+ * @param listener The listener.
+ */
+ public void setVideoDebugListener(VideoRendererEventListener listener) {
+ videoDebugListener = listener;
+ }
+
+ /**
+ * Sets a listener to receive debug events from the audio renderer.
+ *
+ * @param listener The listener.
+ */
+ public void setAudioDebugListener(AudioRendererEventListener listener) {
+ audioDebugListener = listener;
+ }
+
+ // ExoPlayer implementation
+
+ @Override
+ public void addListener(EventListener listener) {
+ player.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(EventListener listener) {
+ player.removeListener(listener);
+ }
+
+ @Override
+ public int getPlaybackState() {
+ return player.getPlaybackState();
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource) {
+ player.prepare(mediaSource);
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ player.prepare(mediaSource, resetPosition, resetState);
+ }
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ player.setPlayWhenReady(playWhenReady);
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ return player.getPlayWhenReady();
+ }
+
+ @Override
+ public boolean isLoading() {
+ return player.isLoading();
+ }
+
+ @Override
+ public void seekToDefaultPosition() {
+ player.seekToDefaultPosition();
+ }
+
+ @Override
+ public void seekToDefaultPosition(int windowIndex) {
+ player.seekToDefaultPosition(windowIndex);
+ }
+
+ @Override
+ public void seekTo(long positionMs) {
+ player.seekTo(positionMs);
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ player.seekTo(windowIndex, positionMs);
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ player.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return player.getPlaybackParameters();
+ }
+
+ @Override
+ public void stop() {
+ player.stop();
+ }
+
+ @Override
+ public void release() {
+ player.release();
+ removeSurfaceCallbacks();
+ if (surface != null) {
+ if (ownsSurface) {
+ surface.release();
+ }
+ surface = null;
+ }
+ }
+
+ @Override
+ public void sendMessages(ExoPlayerMessage... messages) {
+ player.sendMessages(messages);
+ }
+
+ @Override
+ public void blockingSendMessages(ExoPlayerMessage... messages) {
+ player.blockingSendMessages(messages);
+ }
+
+ @Override
+ public int getRendererCount() {
+ return player.getRendererCount();
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ return player.getRendererType(index);
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ return player.getCurrentTrackGroups();
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return player.getCurrentTrackSelections();
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ return player.getCurrentTimeline();
+ }
+
+ @Override
+ public Object getCurrentManifest() {
+ return player.getCurrentManifest();
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ return player.getCurrentPeriodIndex();
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ return player.getCurrentWindowIndex();
+ }
+
+ @Override
+ public long getDuration() {
+ return player.getDuration();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return player.getCurrentPosition();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return player.getBufferedPosition();
+ }
+
+ @Override
+ public int getBufferedPercentage() {
+ return player.getBufferedPercentage();
+ }
+
+ @Override
+ public boolean isCurrentWindowDynamic() {
+ return player.isCurrentWindowDynamic();
+ }
+
+ @Override
+ public boolean isCurrentWindowSeekable() {
+ return player.isCurrentWindowSeekable();
+ }
+
+ // Internal methods.
+
+ private void removeSurfaceCallbacks() {
+ if (textureView != null) {
+ if (textureView.getSurfaceTextureListener() != componentListener) {
+ Log.w(TAG, "SurfaceTextureListener already unset or replaced.");
+ } else {
+ textureView.setSurfaceTextureListener(null);
+ }
+ textureView = null;
+ }
+ if (surfaceHolder != null) {
+ surfaceHolder.removeCallback(componentListener);
+ surfaceHolder = null;
+ }
+ }
+
+ private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) {
+ // Note: We don't turn this method into a no-op if the surface is being replaced with itself
+ // so as to ensure onRenderedFirstFrame callbacks are still called in this case.
+ ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
+ int count = 0;
+ for (Renderer renderer : renderers) {
+ if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
+ messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
+ }
+ }
+ if (this.surface != null && this.surface != surface) {
+ // If we created this surface, we are responsible for releasing it.
+ if (this.ownsSurface) {
+ this.surface.release();
+ }
+ // We're replacing a surface. Block to ensure that it's not accessed after the method returns.
+ player.blockingSendMessages(messages);
+ } else {
+ player.sendMessages(messages);
+ }
+ this.surface = surface;
+ this.ownsSurface = ownsSurface;
+ }
+
+ private final class ComponentListener implements VideoRendererEventListener,
+ AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
+ SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
+
+ // VideoRendererEventListener implementation
+
+ @Override
+ public void onVideoEnabled(DecoderCounters counters) {
+ videoDecoderCounters = counters;
+ if (videoDebugListener != null) {
+ videoDebugListener.onVideoEnabled(counters);
+ }
+ }
+
+ @Override
+ public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
+ long initializationDurationMs) {
+ if (videoDebugListener != null) {
+ videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
+ initializationDurationMs);
+ }
+ }
+
+ @Override
+ public void onVideoInputFormatChanged(Format format) {
+ videoFormat = format;
+ if (videoDebugListener != null) {
+ videoDebugListener.onVideoInputFormatChanged(format);
+ }
+ }
+
+ @Override
+ public void onDroppedFrames(int count, long elapsed) {
+ if (videoDebugListener != null) {
+ videoDebugListener.onDroppedFrames(count, elapsed);
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+ if (videoListener != null) {
+ videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
+ pixelWidthHeightRatio);
+ }
+ if (videoDebugListener != null) {
+ videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
+ pixelWidthHeightRatio);
+ }
+ }
+
+ @Override
+ public void onRenderedFirstFrame(Surface surface) {
+ if (videoListener != null && SimpleExoPlayer.this.surface == surface) {
+ videoListener.onRenderedFirstFrame();
+ }
+ if (videoDebugListener != null) {
+ videoDebugListener.onRenderedFirstFrame(surface);
+ }
+ }
+
+ @Override
+ public void onVideoDisabled(DecoderCounters counters) {
+ if (videoDebugListener != null) {
+ videoDebugListener.onVideoDisabled(counters);
+ }
+ videoFormat = null;
+ videoDecoderCounters = null;
+ }
+
+ // AudioRendererEventListener implementation
+
+ @Override
+ public void onAudioEnabled(DecoderCounters counters) {
+ audioDecoderCounters = counters;
+ if (audioDebugListener != null) {
+ audioDebugListener.onAudioEnabled(counters);
+ }
+ }
+
+ @Override
+ public void onAudioSessionId(int sessionId) {
+ audioSessionId = sessionId;
+ if (audioDebugListener != null) {
+ audioDebugListener.onAudioSessionId(sessionId);
+ }
+ }
+
+ @Override
+ public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
+ long initializationDurationMs) {
+ if (audioDebugListener != null) {
+ audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
+ initializationDurationMs);
+ }
+ }
+
+ @Override
+ public void onAudioInputFormatChanged(Format format) {
+ audioFormat = format;
+ if (audioDebugListener != null) {
+ audioDebugListener.onAudioInputFormatChanged(format);
+ }
+ }
+
+ @Override
+ public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+ long elapsedSinceLastFeedMs) {
+ if (audioDebugListener != null) {
+ audioDebugListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+ }
+
+ @Override
+ public void onAudioDisabled(DecoderCounters counters) {
+ if (audioDebugListener != null) {
+ audioDebugListener.onAudioDisabled(counters);
+ }
+ audioFormat = null;
+ audioDecoderCounters = null;
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ }
+
+ // TextRenderer.Output implementation
+
+ @Override
+ public void onCues(List<Cue> cues) {
+ if (textOutput != null) {
+ textOutput.onCues(cues);
+ }
+ }
+
+ // MetadataRenderer.Output implementation
+
+ @Override
+ public void onMetadata(Metadata metadata) {
+ if (metadataOutput != null) {
+ metadataOutput.onMetadata(metadata);
+ }
+ }
+
+ // SurfaceHolder.Callback implementation
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ setVideoSurfaceInternal(holder.getSurface(), false);
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ // Do nothing.
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ setVideoSurfaceInternal(null, false);
+ }
+
+ // TextureView.SurfaceTextureListener implementation
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+ setVideoSurfaceInternal(new Surface(surfaceTexture), true);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ setVideoSurfaceInternal(null, true);
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+ // Do nothing.
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/Timeline.java
@@ -0,0 +1,441 @@
+/*
+ * 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 com.google.android.exoplayer2;
+
+/**
+ * A representation of media currently available for playback.
+ * <p>
+ * Timeline instances are immutable. For cases where the available media is changing dynamically
+ * (e.g. live streams) a timeline provides a snapshot of the media currently available.
+ * <p>
+ * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single
+ * logical piece of media, for example a media file. A window spans one or more periods, defining
+ * the region within those periods that's currently available for playback along with additional
+ * information such as whether seeking is supported within the window. Each window defines a default
+ * position, which is the position from which playback will start when the player starts playing the
+ * window. The following examples illustrate timelines for various use cases.
+ *
+ * <h3 id="single-file">Single media file or on-demand stream</h3>
+ * <p align="center">
+ * <img src="doc-files/timeline-single-file.svg" alt="Example timeline for a single file">
+ * </p>
+ * A timeline for a single media file or on-demand stream consists of a single period and window.
+ * The window spans the whole period, indicating that all parts of the media are available for
+ * playback. The window's default position is typically at the start of the period (indicated by the
+ * black dot in the figure above).
+ *
+ * <h3>Playlist of media files or on-demand streams</h3>
+ * <p align="center">
+ * <img src="doc-files/timeline-playlist.svg" alt="Example timeline for a playlist of files">
+ * </p>
+ * A timeline for a playlist of media files or on-demand streams consists of multiple periods, each
+ * with its own window. Each window spans the whole of the corresponding period, and typically has a
+ * default position at the start of the period. The properties of the periods and windows (e.g.
+ * their durations and whether the window is seekable) will often only become known when the player
+ * starts buffering the corresponding file or stream.
+ *
+ * <h3 id="live-limited">Live stream with limited availability</h3>
+ * <p align="center">
+ * <img src="doc-files/timeline-live-limited.svg" alt="Example timeline for a live stream with
+ * limited availability">
+ * </p>
+ * A timeline for a live stream consists of a period whose duration is unknown, since it's
+ * continually extending as more content is broadcast. If content only remains available for a
+ * limited period of time then the window may start at a non-zero position, defining the region of
+ * content that can still be played. The window will have {@link Window#isDynamic} set to true if
+ * the stream is still live. Its default position is typically near to the live edge (indicated by
+ * the black dot in the figure above).
+ *
+ * <h3>Live stream with indefinite availability</h3>
+ * <p align="center">
+ * <img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline for a live stream with
+ * indefinite availability">
+ * </p>
+ * A timeline for a live stream with indefinite availability is similar to the
+ * <a href="#live-limited">Live stream with limited availability</a> case, except that the window
+ * starts at the beginning of the period to indicate that all of the previously broadcast content
+ * can still be played.
+ *
+ * <h3 id="live-multi-period">Live stream with multiple periods</h3>
+ * <p align="center">
+ * <img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline for a live stream
+ * with multiple periods">
+ * </p>
+ * This case arises when a live stream is explicitly divided into separate periods, for example at
+ * content and advert boundaries. This case is similar to the <a href="#live-limited">Live stream
+ * with limited availability</a> case, except that the window may span more than one period.
+ * Multiple periods are also possible in the indefinite availability case.
+ *
+ * <h3>On-demand pre-roll followed by live stream</h3>
+ * <p align="center">
+ * <img src="doc-files/timeline-advanced.svg" alt="Example timeline for an on-demand pre-roll
+ * followed by a live stream">
+ * </p>
+ * This case is the concatenation of the <a href="#single-file">Single media file or on-demand
+ * stream</a> and <a href="#multi-period">Live stream with multiple periods</a> cases. When playback
+ * of the pre-roll ends, playback of the live stream will start from its default position near the
+ * live edge.
+ */
+public abstract class Timeline {
+
+ /**
+ * An empty timeline.
+ */
+ public static final Timeline EMPTY = new Timeline() {
+
+ @Override
+ public int getWindowCount() {
+ return 0;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 0;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return C.INDEX_UNSET;
+ }
+
+ };
+
+ /**
+ * Returns whether the timeline is empty.
+ */
+ public final boolean isEmpty() {
+ return getWindowCount() == 0;
+ }
+
+ /**
+ * Returns the number of windows in the timeline.
+ */
+ public abstract int getWindowCount();
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index. Does not populate
+ * {@link Window#id}.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public final Window getWindow(int windowIndex, Window window) {
+ return getWindow(windowIndex, window, false);
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+ * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public Window getWindow(int windowIndex, Window window, boolean setIds) {
+ return getWindow(windowIndex, window, setIds, 0);
+ }
+
+ /**
+ * Populates a {@link Window} with data for the window at the specified index.
+ *
+ * @param windowIndex The index of the window.
+ * @param window The {@link Window} to populate. Must not be null.
+ * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+ * null. The caller should pass false for efficiency reasons unless the field is required.
+ * @param defaultPositionProjectionUs A duration into the future that the populated window's
+ * default start position should be projected.
+ * @return The populated {@link Window}, for convenience.
+ */
+ public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs);
+
+ /**
+ * Returns the number of periods in the timeline.
+ */
+ public abstract int getPeriodCount();
+
+ /**
+ * Populates a {@link Period} with data for the period at the specified index. Does not populate
+ * {@link Period#id} and {@link Period#uid}.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public final Period getPeriod(int periodIndex, Period period) {
+ return getPeriod(periodIndex, period, false);
+ }
+
+ /**
+ * Populates a {@link Period} with data for the period at the specified index.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,
+ * the fields will be set to null. The caller should pass false for efficiency reasons unless
+ * the fields are required.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);
+
+ /**
+ * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET}
+ * if the period is not in the timeline.
+ *
+ * @param uid A unique identifier for a period.
+ * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.
+ */
+ public abstract int getIndexOfPeriod(Object uid);
+
+ /**
+ * Holds information about a window in a {@link Timeline}. A window defines a region of media
+ * currently available for playback along with additional information such as whether seeking is
+ * supported within the window. See {@link Timeline} for more details. The figure below shows some
+ * of the information defined by a window, as well as how this information relates to
+ * corresponding {@link Period}s in the timeline.
+ * <p align="center">
+ * <img src="doc-files/timeline-window.svg" alt="Information defined by a timeline window">
+ * </p>
+ */
+ public static final class Window {
+
+ /**
+ * An identifier for the window. Not necessarily unique.
+ */
+ public Object id;
+
+ /**
+ * The start time of the presentation to which this window belongs in milliseconds since the
+ * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only.
+ */
+ public long presentationStartTimeMs;
+
+ /**
+ * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown
+ * or not applicable. For informational purposes only.
+ */
+ public long windowStartTimeMs;
+
+ /**
+ * Whether it's possible to seek within this window.
+ */
+ public boolean isSeekable;
+
+ /**
+ * Whether this window may change when the timeline is updated.
+ */
+ public boolean isDynamic;
+
+ /**
+ * The index of the first period that belongs to this window.
+ */
+ public int firstPeriodIndex;
+
+ /**
+ * The index of the last period that belongs to this window.
+ */
+ public int lastPeriodIndex;
+
+ /**
+ * The default position relative to the start of the window at which to begin playback, in
+ * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
+ * non-zero default position projection, and if the specified projection cannot be performed
+ * whilst remaining within the bounds of the window.
+ */
+ public long defaultPositionUs;
+
+ /**
+ * The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long durationUs;
+
+ /**
+ * The position of the start of this window relative to the start of the first period belonging
+ * to it, in microseconds.
+ */
+ public long positionInFirstPeriodUs;
+
+ /**
+ * Sets the data held by this window.
+ */
+ public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs,
+ boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs,
+ int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) {
+ this.id = id;
+ this.presentationStartTimeMs = presentationStartTimeMs;
+ this.windowStartTimeMs = windowStartTimeMs;
+ this.isSeekable = isSeekable;
+ this.isDynamic = isDynamic;
+ this.defaultPositionUs = defaultPositionUs;
+ this.durationUs = durationUs;
+ this.firstPeriodIndex = firstPeriodIndex;
+ this.lastPeriodIndex = lastPeriodIndex;
+ this.positionInFirstPeriodUs = positionInFirstPeriodUs;
+ return this;
+ }
+
+ /**
+ * Returns the default position relative to the start of the window at which to begin playback,
+ * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
+ * non-zero default position projection, and if the specified projection cannot be performed
+ * whilst remaining within the bounds of the window.
+ */
+ public long getDefaultPositionMs() {
+ return C.usToMs(defaultPositionUs);
+ }
+
+ /**
+ * Returns the default position relative to the start of the window at which to begin playback,
+ * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
+ * non-zero default position projection, and if the specified projection cannot be performed
+ * whilst remaining within the bounds of the window.
+ */
+ public long getDefaultPositionUs() {
+ return defaultPositionUs;
+ }
+
+ /**
+ * Returns the duration of the window in milliseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationMs() {
+ return C.usToMs(durationUs);
+ }
+
+ /**
+ * Returns the duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the position of the start of this window relative to the start of the first period
+ * belonging to it, in milliseconds.
+ */
+ public long getPositionInFirstPeriodMs() {
+ return C.usToMs(positionInFirstPeriodUs);
+ }
+
+ /**
+ * Returns the position of the start of this window relative to the start of the first period
+ * belonging to it, in microseconds.
+ */
+ public long getPositionInFirstPeriodUs() {
+ return positionInFirstPeriodUs;
+ }
+
+ }
+
+ /**
+ * Holds information about a period in a {@link Timeline}. A period defines a single logical piece
+ * of media, for example a a media file. See {@link Timeline} for more details. The figure below
+ * shows some of the information defined by a period, as well as how this information relates to a
+ * corresponding {@link Window} in the timeline.
+ * <p align="center">
+ * <img src="doc-files/timeline-period.svg" alt="Information defined by a period">
+ * </p>
+ */
+ public static final class Period {
+
+ /**
+ * An identifier for the period. Not necessarily unique.
+ */
+ public Object id;
+
+ /**
+ * A unique identifier for the period.
+ */
+ public Object uid;
+
+ /**
+ * The index of the window to which this period belongs.
+ */
+ public int windowIndex;
+
+ /**
+ * The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long durationUs;
+
+ /**
+ * Whether this period contains an ad.
+ */
+ public boolean isAd;
+
+ private long positionInWindowUs;
+
+ /**
+ * Sets the data held by this period.
+ */
+ public Period set(Object id, Object uid, int windowIndex, long durationUs,
+ long positionInWindowUs, boolean isAd) {
+ this.id = id;
+ this.uid = uid;
+ this.windowIndex = windowIndex;
+ this.durationUs = durationUs;
+ this.positionInWindowUs = positionInWindowUs;
+ this.isAd = isAd;
+ return this;
+ }
+
+ /**
+ * Returns the duration of the period in milliseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationMs() {
+ return C.usToMs(durationUs);
+ }
+
+ /**
+ * Returns the duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the position of the start of this period relative to the start of the window to which
+ * it belongs, in milliseconds. May be negative if the start of the period is not within the
+ * window.
+ */
+ public long getPositionInWindowMs() {
+ return C.usToMs(positionInWindowUs);
+ }
+
+ /**
+ * Returns the position of the start of this period relative to the start of the window to which
+ * it belongs, in microseconds. May be negative if the start of the period is not within the
+ * window.
+ */
+ public long getPositionInWindowUs() {
+ return positionInWindowUs;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -0,0 +1,269 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams.
+ */
+public final class Ac3Util {
+
+ /**
+ * Holds sample format information as presented by a syncframe header.
+ */
+ public static final class Ac3SyncFrameInfo {
+
+ /**
+ * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and
+ * {@link MimeTypes#AUDIO_E_AC3}.
+ */
+ public final String mimeType;
+ /**
+ * The audio sampling rate in Hz.
+ */
+ public final int sampleRate;
+ /**
+ * The number of audio channels
+ */
+ public final int channelCount;
+ /**
+ * The size of the frame.
+ */
+ public final int frameSize;
+ /**
+ * Number of audio samples in the frame.
+ */
+ public final int sampleCount;
+
+ private Ac3SyncFrameInfo(String mimeType, int channelCount, int sampleRate, int frameSize,
+ int sampleCount) {
+ this.mimeType = mimeType;
+ this.channelCount = channelCount;
+ this.sampleRate = sampleRate;
+ this.frameSize = frameSize;
+ this.sampleCount = sampleCount;
+ }
+
+ }
+
+ /**
+ * The number of new samples per (E-)AC-3 audio block.
+ */
+ private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;
+ /**
+ * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.
+ */
+ private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK;
+ /**
+ * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod.
+ */
+ private static final int[] BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD = new int[] {1, 2, 3, 6};
+ /**
+ * Sample rates, indexed by fscod.
+ */
+ private static final int[] SAMPLE_RATE_BY_FSCOD = new int[] {48000, 44100, 32000};
+ /**
+ * Sample rates, indexed by fscod2 (E-AC-3).
+ */
+ private static final int[] SAMPLE_RATE_BY_FSCOD2 = new int[] {24000, 22050, 16000};
+ /**
+ * Channel counts, indexed by acmod.
+ */
+ private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
+ /**
+ * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)
+ */
+ private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96,
+ 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640};
+ /**
+ * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)
+ */
+ private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104,
+ 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};
+
+ /**
+ * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to
+ * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified.
+ *
+ * @param data The AC3SpecificBox to parse.
+ * @param trackId The track identifier to set on the format, or null.
+ * @param language The language to set on the format.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @return The AC-3 format parsed from data in the header.
+ */
+ public static Format parseAc3AnnexFFormat(ParsableByteArray data, String trackId,
+ String language, DrmInitData drmInitData) {
+ int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
+ int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ int nextByte = data.readUnsignedByte();
+ int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x38) >> 3];
+ if ((nextByte & 0x04) != 0) { // lfeon
+ channelCount++;
+ }
+ return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE,
+ Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+ }
+
+ /**
+ * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to
+ * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified.
+ *
+ * @param data The EC3SpecificBox to parse.
+ * @param trackId The track identifier to set on the format, or null.
+ * @param language The language to set on the format.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @return The E-AC-3 format parsed from data in the header.
+ */
+ public static Format parseEAc3AnnexFFormat(ParsableByteArray data, String trackId,
+ String language, DrmInitData drmInitData) {
+ data.skipBytes(2); // data_rate, num_ind_sub
+
+ // Read only the first substream.
+ // TODO: Read later substreams?
+ int fscod = (data.readUnsignedByte() & 0xC0) >> 6;
+ int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ int nextByte = data.readUnsignedByte();
+ int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x0E) >> 1];
+ if ((nextByte & 0x01) != 0) { // lfeon
+ channelCount++;
+ }
+ return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE,
+ Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+ }
+
+ /**
+ * Returns (E-)AC-3 format information given {@code data} containing a syncframe. The reading
+ * position of {@code data} will be modified.
+ *
+ * @param data The data to parse, positioned at the start of the syncframe.
+ * @return The (E-)AC-3 format data parsed from the header.
+ */
+ public static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
+ int initialPosition = data.getPosition();
+ data.skipBits(40);
+ boolean isEac3 = data.readBits(5) == 16;
+ data.setPosition(initialPosition);
+ String mimeType;
+ int sampleRate;
+ int acmod;
+ int frameSize;
+ int sampleCount;
+ if (isEac3) {
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ data.skipBits(16 + 2 + 3); // syncword, strmtype, substreamid
+ frameSize = (data.readBits(11) + 1) * 2;
+ int fscod = data.readBits(2);
+ int audioBlocks;
+ if (fscod == 3) {
+ sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)];
+ audioBlocks = 6;
+ } else {
+ int numblkscod = data.readBits(2);
+ audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod];
+ sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ }
+ sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks;
+ acmod = data.readBits(3);
+ } else /* is AC-3 */ {
+ mimeType = MimeTypes.AUDIO_AC3;
+ data.skipBits(16 + 16); // syncword, crc1
+ int fscod = data.readBits(2);
+ int frmsizecod = data.readBits(6);
+ frameSize = getAc3SyncframeSize(fscod, frmsizecod);
+ data.skipBits(5 + 3); // bsid, bsmod
+ acmod = data.readBits(3);
+ if ((acmod & 0x01) != 0 && acmod != 1) {
+ data.skipBits(2); // cmixlev
+ }
+ if ((acmod & 0x04) != 0) {
+ data.skipBits(2); // surmixlev
+ }
+ if (acmod == 2) {
+ data.skipBits(2); // dsurmod
+ }
+ sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
+ }
+ boolean lfeon = data.readBit();
+ int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
+ return new Ac3SyncFrameInfo(mimeType, channelCount, sampleRate, frameSize, sampleCount);
+ }
+
+ /**
+ * Returns the size in bytes of the given AC-3 syncframe.
+ *
+ * @param data The syncframe to parse.
+ * @return The syncframe size in bytes. {@link C#LENGTH_UNSET} if the input is invalid.
+ */
+ public static int parseAc3SyncframeSize(byte[] data) {
+ if (data.length < 5) {
+ return C.LENGTH_UNSET;
+ }
+ int fscod = (data[4] & 0xC0) >> 6;
+ int frmsizecod = data[4] & 0x3F;
+ return getAc3SyncframeSize(fscod, frmsizecod);
+ }
+
+ /**
+ * Returns the number of audio samples in an AC-3 syncframe.
+ */
+ public static int getAc3SyncframeAudioSampleCount() {
+ return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
+ }
+
+ /**
+ * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's
+ * position is not modified.
+ *
+ * @param buffer The {@link ByteBuffer} from which to read the syncframe.
+ * @return The number of audio samples represented by the syncframe.
+ */
+ public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
+ // See ETSI TS 102 366 subsection E.1.2.2.
+ int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;
+ return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6
+ : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);
+ }
+
+ private static int getAc3SyncframeSize(int fscod, int frmsizecod) {
+ int halfFrmsizecod = frmsizecod / 2;
+ if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0
+ || halfFrmsizecod >= SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1.length) {
+ // Invalid values provided.
+ return C.LENGTH_UNSET;
+ }
+ int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+ if (sampleRate == 44100) {
+ return 2 * (SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1[halfFrmsizecod] + (frmsizecod % 2));
+ }
+ int bitrate = BITRATE_BY_HALF_FRMSIZECOD[halfFrmsizecod];
+ if (sampleRate == 32000) {
+ return 6 * bitrate;
+ } else { // sampleRate == 48000
+ return 4 * bitrate;
+ }
+ }
+
+ private Ac3Util() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import java.util.Arrays;
+
+/**
+ * Represents the set of audio formats that a device is capable of playing.
+ */
+@TargetApi(21)
+public final class AudioCapabilities {
+
+ /**
+ * The minimum audio capabilities supported by all devices.
+ */
+ public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
+ new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);
+
+ /**
+ * Returns the current audio capabilities for the device.
+ *
+ * @param context A context for obtaining the current audio capabilities.
+ * @return The current audio capabilities for the device.
+ */
+ @SuppressWarnings("InlinedApi")
+ public static AudioCapabilities getCapabilities(Context context) {
+ return getCapabilities(
+ context.registerReceiver(null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)));
+ }
+
+ @SuppressLint("InlinedApi")
+ /* package */ static AudioCapabilities getCapabilities(Intent intent) {
+ if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {
+ return DEFAULT_AUDIO_CAPABILITIES;
+ }
+ return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
+ intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0));
+ }
+
+ private final int[] supportedEncodings;
+ private final int maxChannelCount;
+
+ /**
+ * Constructs new audio capabilities based on a set of supported encodings and a maximum channel
+ * count.
+ *
+ * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
+ * {@code ENCODING_*} constants.
+ * @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
+ */
+ /* package */ AudioCapabilities(int[] supportedEncodings, int maxChannelCount) {
+ if (supportedEncodings != null) {
+ this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);
+ Arrays.sort(this.supportedEncodings);
+ } else {
+ this.supportedEncodings = new int[0];
+ }
+ this.maxChannelCount = maxChannelCount;
+ }
+
+ /**
+ * Returns whether this device supports playback of the specified audio {@code encoding}.
+ *
+ * @param encoding One of {@link android.media.AudioFormat}'s {@code ENCODING_*} constants.
+ * @return Whether this device supports playback the specified audio {@code encoding}.
+ */
+ public boolean supportsEncoding(int encoding) {
+ return Arrays.binarySearch(supportedEncodings, encoding) >= 0;
+ }
+
+ /**
+ * Returns the maximum number of channels the device can play at the same time.
+ */
+ public int getMaxChannelCount() {
+ return maxChannelCount;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof AudioCapabilities)) {
+ return false;
+ }
+ AudioCapabilities audioCapabilities = (AudioCapabilities) other;
+ return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings)
+ && maxChannelCount == audioCapabilities.maxChannelCount;
+ }
+
+ @Override
+ public int hashCode() {
+ return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings);
+ }
+
+ @Override
+ public String toString() {
+ return "AudioCapabilities[maxChannelCount=" + maxChannelCount
+ + ", supportedEncodings=" + Arrays.toString(supportedEncodings) + "]";
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.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 com.google.android.exoplayer2.audio;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Receives broadcast events indicating changes to the device's audio capabilities, notifying a
+ * {@link Listener} when audio capability changes occur.
+ */
+public final class AudioCapabilitiesReceiver {
+
+ /**
+ * Listener notified when audio capabilities change.
+ */
+ public interface Listener {
+
+ /**
+ * Called when the audio capabilities change.
+ *
+ * @param audioCapabilities The current audio capabilities for the device.
+ */
+ void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities);
+
+ }
+
+ private final Context context;
+ private final Listener listener;
+ private final BroadcastReceiver receiver;
+
+ /* package */ AudioCapabilities audioCapabilities;
+
+ /**
+ * @param context A context for registering the receiver.
+ * @param listener The listener to notify when audio capabilities change.
+ */
+ public AudioCapabilitiesReceiver(Context context, Listener listener) {
+ this.context = Assertions.checkNotNull(context);
+ this.listener = Assertions.checkNotNull(listener);
+ this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null;
+ }
+
+ /**
+ * Registers the receiver, meaning it will notify the listener when audio capability changes
+ * occur. The current audio capabilities will be returned. It is important to call
+ * {@link #unregister} when the receiver is no longer required.
+ *
+ * @return The current audio capabilities for the device.
+ */
+ @SuppressWarnings("InlinedApi")
+ public AudioCapabilities register() {
+ Intent stickyIntent = receiver == null ? null
+ : context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
+ audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent);
+ return audioCapabilities;
+ }
+
+ /**
+ * Unregisters the receiver, meaning it will no longer notify the listener when audio capability
+ * changes occur.
+ */
+ public void unregister() {
+ if (receiver != null) {
+ context.unregisterReceiver(receiver);
+ }
+ }
+
+ private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!isInitialStickyBroadcast()) {
+ AudioCapabilities newAudioCapabilities = AudioCapabilities.getCapabilities(intent);
+ if (!newAudioCapabilities.equals(audioCapabilities)) {
+ audioCapabilities = newAudioCapabilities;
+ listener.onAudioCapabilitiesChanged(newAudioCapabilities);
+ }
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/AudioDecoderException.java
@@ -0,0 +1,41 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+/**
+ * Thrown when an audio decoder error occurs.
+ */
+public abstract class AudioDecoderException extends Exception {
+
+ /**
+ * @param detailMessage The detail message for this exception.
+ */
+ public AudioDecoderException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ /**
+ * @param detailMessage The detail message for this exception.
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link #getCause()} method). (A <tt>null</tt> value is
+ * permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ */
+ public AudioDecoderException(String detailMessage, Throwable cause) {
+ super(detailMessage, cause);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/AudioProcessor.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Interface for audio processors.
+ */
+public interface AudioProcessor {
+
+ /**
+ * Exception thrown when a processor can't be configured for a given input audio format.
+ */
+ final class UnhandledFormatException extends Exception {
+
+ public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
+ super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding "
+ + encoding);
+ }
+
+ }
+
+ /**
+ * An empty, direct {@link ByteBuffer}.
+ */
+ ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
+
+ /**
+ * Configures the processor to process input audio with the specified format. After calling this
+ * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the
+ * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the
+ * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a
+ * result of the call. If it's active, {@link #getOutputChannelCount()} and
+ * {@link #getOutputEncoding()} return the processor's output format.
+ *
+ * @param sampleRateHz The sample rate of input audio in Hz.
+ * @param channelCount The number of interleaved channels in input audio.
+ * @param encoding The encoding of input audio.
+ * @return {@code true} if the processor must be flushed or the value returned by
+ * {@link #isActive()} has changed as a result of the call.
+ * @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
+ */
+ boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
+ throws UnhandledFormatException;
+
+ /**
+ * Returns whether the processor is configured and active.
+ */
+ boolean isActive();
+
+ /**
+ * Returns the number of audio channels in the data output by the processor.
+ */
+ int getOutputChannelCount();
+
+ /**
+ * Returns the audio encoding used in the data output by the processor.
+ */
+ @C.Encoding
+ int getOutputEncoding();
+
+ /**
+ * Queues audio data between the position and limit of the input {@code buffer} for processing.
+ * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as
+ * read-only. Its position will be advanced by the number of bytes consumed (which may be zero).
+ * The caller retains ownership of the provided buffer. Calling this method invalidates any
+ * previous buffer returned by {@link #getOutput()}.
+ *
+ * @param buffer The input buffer to process.
+ */
+ void queueInput(ByteBuffer buffer);
+
+ /**
+ * Queues an end of stream signal. After this method has been called,
+ * {@link #queueInput(ByteBuffer)} may not be called until after the next call to
+ * {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple
+ * calls may be required to read all of the remaining output data. {@link #isEnded()} will return
+ * {@code true} once all remaining output data has been read.
+ */
+ void queueEndOfStream();
+
+ /**
+ * Returns a buffer containing processed output data between its position and limit. The buffer
+ * will always be a direct byte buffer with native byte order. Calling this method invalidates any
+ * previously returned buffer. The buffer will be empty if no output is available.
+ *
+ * @return A buffer containing processed output data between its position and limit.
+ */
+ ByteBuffer getOutput();
+
+ /**
+ * Returns whether this processor will return no more output from {@link #getOutput()} until it
+ * has been {@link #flush()}ed and more input has been queued.
+ */
+ boolean isEnded();
+
+ /**
+ * Clears any state in preparation for receiving a new stream of input buffers.
+ */
+ void flush();
+
+ /**
+ * Resets the processor to its initial state.
+ */
+ void reset();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
@@ -0,0 +1,190 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Listener of audio {@link Renderer} events.
+ */
+public interface AudioRendererEventListener {
+
+ /**
+ * Called when the renderer is enabled.
+ *
+ * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
+ * remains enabled.
+ */
+ void onAudioEnabled(DecoderCounters counters);
+
+ /**
+ * Called when the audio session is set.
+ *
+ * @param audioSessionId The audio session id.
+ */
+ void onAudioSessionId(int audioSessionId);
+
+ /**
+ * Called when a decoder is created.
+ *
+ * @param decoderName The decoder that was created.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
+ */
+ void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
+ long initializationDurationMs);
+
+ /**
+ * Called when the format of the media being consumed by the renderer changes.
+ *
+ * @param format The new format.
+ */
+ void onAudioInputFormatChanged(Format format);
+
+ /**
+ * Called when an {@link AudioTrack} underrun occurs.
+ *
+ * @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes.
+ * @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is
+ * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,
+ * as the buffered media can have a variable bitrate so the duration may be unknown.
+ * @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data.
+ */
+ void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
+
+ /**
+ * Called when the renderer is disabled.
+ *
+ * @param counters {@link DecoderCounters} that were updated by the renderer.
+ */
+ void onAudioDisabled(DecoderCounters counters);
+
+ /**
+ * Dispatches events to a {@link AudioRendererEventListener}.
+ */
+ final class EventDispatcher {
+
+ private final Handler handler;
+ private final AudioRendererEventListener listener;
+
+ /**
+ * @param handler A handler for dispatching events, or null if creating a dummy instance.
+ * @param listener The listener to which events should be dispatched, or null if creating a
+ * dummy instance.
+ */
+ public EventDispatcher(Handler handler, AudioRendererEventListener listener) {
+ this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+ this.listener = listener;
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.
+ */
+ public void enabled(final DecoderCounters decoderCounters) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onAudioEnabled(decoderCounters);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.
+ */
+ public void decoderInitialized(final String decoderName,
+ final long initializedTimestampMs, final long initializationDurationMs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
+ initializationDurationMs);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.
+ */
+ public void inputFormatChanged(final Format format) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onAudioInputFormatChanged(format);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioTrackUnderrun(int, long, long)}.
+ */
+ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs,
+ final long elapsedSinceLastFeedMs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
+ */
+ public void disabled(final DecoderCounters counters) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ counters.ensureUpdated();
+ listener.onAudioDisabled(counters);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.
+ */
+ public void audioSessionId(final int audioSessionId) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onAudioSessionId(audioSessionId);
+ }
+ });
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -0,0 +1,1732 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioTimestamp;
+import android.os.ConditionVariable;
+import android.os.SystemClock;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+/**
+ * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
+ * playback position smoothing, non-blocking writes and reconfiguration.
+ * <p>
+ * Before starting playback, specify the input format by calling
+ * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
+ * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
+ * to configure audio playback. These methods may be called after writing data to the track, in
+ * which case it will be reinitialized as required.
+ * <p>
+ * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
+ * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
+ * <p>
+ * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track
+ * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
+ * <p>
+ * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does
+ * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is
+ * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
+ * {@link #configure(String, int, int, int, int)}.
+ * <p>
+ * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will
+ * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call
+ * {@link #release()} when the instance is no longer required.
+ */
+public final class AudioTrack {
+
+ /**
+ * Listener for audio track events.
+ */
+ public interface Listener {
+
+ /**
+ * Called when the audio track has been initialized with a newly generated audio session id.
+ *
+ * @param audioSessionId The newly generated audio session id.
+ */
+ void onAudioSessionId(int audioSessionId);
+
+ /**
+ * Called when the audio track handles a buffer whose timestamp is discontinuous with the last
+ * buffer handled since it was reset.
+ */
+ void onPositionDiscontinuity();
+
+ /**
+ * Called when the audio track underruns.
+ *
+ * @param bufferSize The size of the track's buffer, in bytes.
+ * @param bufferSizeMs The size of the track's buffer, in milliseconds, if it is configured for
+ * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the
+ * buffered media can have a variable bitrate so the duration may be unknown.
+ * @param elapsedSinceLastFeedMs The time since the track was last fed data, in milliseconds.
+ */
+ void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
+
+ }
+
+ /**
+ * Thrown when a failure occurs configuring the track.
+ */
+ public static final class ConfigurationException extends Exception {
+
+ public ConfigurationException(Throwable cause) {
+ super(cause);
+ }
+
+ public ConfigurationException(String message) {
+ super(message);
+ }
+
+ }
+
+ /**
+ * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}.
+ */
+ public static final class InitializationException extends Exception {
+
+ /**
+ * The state as reported by {@link android.media.AudioTrack#getState()}.
+ */
+ public final int audioTrackState;
+
+ /**
+ * @param audioTrackState The state as reported by {@link android.media.AudioTrack#getState()}.
+ * @param sampleRate The requested sample rate in Hz.
+ * @param channelConfig The requested channel configuration.
+ * @param bufferSize The requested buffer size in bytes.
+ */
+ public InitializationException(int audioTrackState, int sampleRate, int channelConfig,
+ int bufferSize) {
+ super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", "
+ + channelConfig + ", " + bufferSize + ")");
+ this.audioTrackState = audioTrackState;
+ }
+
+ }
+
+ /**
+ * Thrown when a failure occurs writing to an {@link android.media.AudioTrack}.
+ */
+ public static final class WriteException extends Exception {
+
+ /**
+ * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or
+ * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
+ */
+ public final int errorCode;
+
+ /**
+ * @param errorCode The error value returned from
+ * {@link android.media.AudioTrack#write(byte[], int, int)} or
+ * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
+ */
+ public WriteException(int errorCode) {
+ super("AudioTrack write failed: " + errorCode);
+ this.errorCode = errorCode;
+ }
+
+ }
+
+ /**
+ * Thrown when {@link android.media.AudioTrack#getTimestamp} returns a spurious timestamp, if
+ * {@code AudioTrack#failOnSpuriousAudioTimestamp} is set.
+ */
+ public static final class InvalidAudioTrackTimestampException extends RuntimeException {
+
+ /**
+ * @param detailMessage The detail message for this exception.
+ */
+ public InvalidAudioTrackTimestampException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ }
+
+ /**
+ * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set.
+ */
+ public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
+
+ /**
+ * A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds.
+ */
+ private static final long MIN_BUFFER_DURATION_US = 250000;
+ /**
+ * A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds.
+ */
+ private static final long MAX_BUFFER_DURATION_US = 750000;
+ /**
+ * The length for passthrough {@link android.media.AudioTrack} buffers, in microseconds.
+ */
+ private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000;
+ /**
+ * A multiplication factor to apply to the minimum buffer size requested by the underlying
+ * {@link android.media.AudioTrack}.
+ */
+ private static final int BUFFER_MULTIPLICATION_FACTOR = 4;
+
+ /**
+ * @see android.media.AudioTrack#PLAYSTATE_STOPPED
+ */
+ private static final int PLAYSTATE_STOPPED = android.media.AudioTrack.PLAYSTATE_STOPPED;
+ /**
+ * @see android.media.AudioTrack#PLAYSTATE_PAUSED
+ */
+ private static final int PLAYSTATE_PAUSED = android.media.AudioTrack.PLAYSTATE_PAUSED;
+ /**
+ * @see android.media.AudioTrack#PLAYSTATE_PLAYING
+ */
+ private static final int PLAYSTATE_PLAYING = android.media.AudioTrack.PLAYSTATE_PLAYING;
+ /**
+ * @see android.media.AudioTrack#ERROR_BAD_VALUE
+ */
+ private static final int ERROR_BAD_VALUE = android.media.AudioTrack.ERROR_BAD_VALUE;
+ /**
+ * @see android.media.AudioTrack#MODE_STATIC
+ */
+ private static final int MODE_STATIC = android.media.AudioTrack.MODE_STATIC;
+ /**
+ * @see android.media.AudioTrack#MODE_STREAM
+ */
+ private static final int MODE_STREAM = android.media.AudioTrack.MODE_STREAM;
+ /**
+ * @see android.media.AudioTrack#STATE_INITIALIZED
+ */
+ private static final int STATE_INITIALIZED = android.media.AudioTrack.STATE_INITIALIZED;
+ /**
+ * @see android.media.AudioTrack#WRITE_NON_BLOCKING
+ */
+ @SuppressLint("InlinedApi")
+ private static final int WRITE_NON_BLOCKING = android.media.AudioTrack.WRITE_NON_BLOCKING;
+
+ private static final String TAG = "AudioTrack";
+
+ /**
+ * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
+ * than this amount.
+ * <p>
+ * This is a fail safe that should not be required on correctly functioning devices.
+ */
+ private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
+
+ /**
+ * AudioTrack latencies are deemed impossibly large if they are greater than this amount.
+ * <p>
+ * This is a fail safe that should not be required on correctly functioning devices.
+ */
+ private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
+
+ private static final int START_NOT_SET = 0;
+ private static final int START_IN_SYNC = 1;
+ private static final int START_NEED_SYNC = 2;
+
+ private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
+ private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
+ private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
+
+ /**
+ * The minimum number of output bytes from {@link #sonicAudioProcessor} at which the speedup is
+ * calculated using the input/output byte counts from the processor, rather than using the
+ * current playback parameters speed.
+ */
+ private static final int SONIC_MIN_BYTES_FOR_SPEEDUP = 1024;
+
+ /**
+ * Whether to enable a workaround for an issue where an audio effect does not keep its session
+ * active across releasing/initializing a new audio track, on platform builds where
+ * {@link Util#SDK_INT} < 21.
+ * <p>
+ * The flag must be set before creating a player.
+ */
+ public static boolean enablePreV21AudioSessionWorkaround = false;
+
+ /**
+ * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is
+ * reported from {@link android.media.AudioTrack#getTimestamp}.
+ * <p>
+ * The flag must be set before creating a player. Should be set to {@code true} for testing and
+ * debugging purposes only.
+ */
+ public static boolean failOnSpuriousAudioTimestamp = false;
+
+ private final AudioCapabilities audioCapabilities;
+ private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
+ private final SonicAudioProcessor sonicAudioProcessor;
+ private final AudioProcessor[] availableAudioProcessors;
+ private final Listener listener;
+ private final ConditionVariable releasingConditionVariable;
+ private final long[] playheadOffsets;
+ private final AudioTrackUtil audioTrackUtil;
+ private final LinkedList<PlaybackParametersCheckpoint> playbackParametersCheckpoints;
+
+ /**
+ * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
+ */
+ private android.media.AudioTrack keepSessionIdAudioTrack;
+
+ private android.media.AudioTrack audioTrack;
+ private int sampleRate;
+ private int channelConfig;
+ @C.Encoding
+ private int encoding;
+ @C.Encoding
+ private int outputEncoding;
+ @C.StreamType
+ private int streamType;
+ private boolean passthrough;
+ private int bufferSize;
+ private long bufferSizeUs;
+
+ private PlaybackParameters drainingPlaybackParameters;
+ private PlaybackParameters playbackParameters;
+ private long playbackParametersOffsetUs;
+ private long playbackParametersPositionUs;
+
+ private ByteBuffer avSyncHeader;
+ private int bytesUntilNextAvSync;
+
+ private int nextPlayheadOffsetIndex;
+ private int playheadOffsetCount;
+ private long smoothedPlayheadOffsetUs;
+ private long lastPlayheadSampleTimeUs;
+ private boolean audioTimestampSet;
+ private long lastTimestampSampleTimeUs;
+
+ private Method getLatencyMethod;
+ private int pcmFrameSize;
+ private long submittedPcmBytes;
+ private long submittedEncodedFrames;
+ private int outputPcmFrameSize;
+ private long writtenPcmBytes;
+ private long writtenEncodedFrames;
+ private int framesPerEncodedSample;
+ private int startMediaTimeState;
+ private long startMediaTimeUs;
+ private long resumeSystemTimeUs;
+ private long latencyUs;
+ private float volume;
+
+ private AudioProcessor[] audioProcessors;
+ private ByteBuffer[] outputBuffers;
+ private ByteBuffer inputBuffer;
+ private ByteBuffer outputBuffer;
+ private byte[] preV21OutputBuffer;
+ private int preV21OutputBufferOffset;
+ private int drainingAudioProcessorIndex;
+ private boolean handledEndOfStream;
+
+ private boolean playing;
+ private int audioSessionId;
+ private boolean tunneling;
+ private boolean hasData;
+ private long lastFeedElapsedRealtimeMs;
+
+ /**
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
+ * output. May be empty.
+ * @param listener Listener for audio track events.
+ */
+ public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors,
+ Listener listener) {
+ this.audioCapabilities = audioCapabilities;
+ this.listener = listener;
+ releasingConditionVariable = new ConditionVariable(true);
+ if (Util.SDK_INT >= 18) {
+ try {
+ getLatencyMethod =
+ android.media.AudioTrack.class.getMethod("getLatency", (Class<?>[]) null);
+ } catch (NoSuchMethodException e) {
+ // There's no guarantee this method exists. Do nothing.
+ }
+ }
+ if (Util.SDK_INT >= 19) {
+ audioTrackUtil = new AudioTrackUtilV19();
+ } else {
+ audioTrackUtil = new AudioTrackUtil();
+ }
+ channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
+ sonicAudioProcessor = new SonicAudioProcessor();
+ availableAudioProcessors = new AudioProcessor[3 + audioProcessors.length];
+ availableAudioProcessors[0] = new ResamplingAudioProcessor();
+ availableAudioProcessors[1] = channelMappingAudioProcessor;
+ System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length);
+ availableAudioProcessors[2 + audioProcessors.length] = sonicAudioProcessor;
+ playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
+ volume = 1.0f;
+ startMediaTimeState = START_NOT_SET;
+ streamType = C.STREAM_TYPE_DEFAULT;
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ playbackParameters = PlaybackParameters.DEFAULT;
+ drainingAudioProcessorIndex = C.INDEX_UNSET;
+ this.audioProcessors = new AudioProcessor[0];
+ outputBuffers = new ByteBuffer[0];
+ playbackParametersCheckpoints = new LinkedList<>();
+ }
+
+ /**
+ * Returns whether it's possible to play audio in the specified format using encoded passthrough.
+ *
+ * @param mimeType The format mime type.
+ * @return Whether it's possible to play audio in the format using encoded passthrough.
+ */
+ public boolean isPassthroughSupported(String mimeType) {
+ return audioCapabilities != null
+ && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
+ }
+
+ /**
+ * Returns the playback position in the stream starting at zero, in microseconds, or
+ * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
+ *
+ * <p>If the device supports it, the method uses the playback timestamp from
+ * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by
+ * sampling the {@link android.media.AudioTrack}'s frame position.
+ *
+ * @param sourceEnded Specify {@code true} if no more input buffers will be provided.
+ * @return The playback position relative to the start of playback, in microseconds.
+ */
+ public long getCurrentPositionUs(boolean sourceEnded) {
+ if (!hasCurrentPositionUs()) {
+ return CURRENT_POSITION_NOT_SET;
+ }
+
+ if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
+ maybeSampleSyncParams();
+ }
+
+ long systemClockUs = System.nanoTime() / 1000;
+ long positionUs;
+ if (audioTimestampSet) {
+ // Calculate the speed-adjusted position using the timestamp (which may be in the future).
+ long elapsedSinceTimestampUs = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000);
+ long elapsedSinceTimestampFrames = durationUsToFrames(elapsedSinceTimestampUs);
+ long elapsedFrames = audioTrackUtil.getTimestampFramePosition() + elapsedSinceTimestampFrames;
+ positionUs = framesToDurationUs(elapsedFrames);
+ } else {
+ if (playheadOffsetCount == 0) {
+ // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
+ positionUs = audioTrackUtil.getPositionUs();
+ } else {
+ // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
+ // system clock (and a smoothed offset between it and the playhead position) so as to
+ // prevent jitter in the reported positions.
+ positionUs = systemClockUs + smoothedPlayheadOffsetUs;
+ }
+ if (!sourceEnded) {
+ positionUs -= latencyUs;
+ }
+ }
+
+ return startMediaTimeUs + applySpeedup(positionUs);
+ }
+
+ /**
+ * Configures (or reconfigures) the audio track.
+ *
+ * @param mimeType The mime type.
+ * @param channelCount The number of channels.
+ * @param sampleRate The sample rate in Hz.
+ * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT},
+ * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
+ * {@link C#ENCODING_PCM_32BIT}.
+ * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
+ * suitable buffer size automatically.
+ * @throws ConfigurationException If an error occurs configuring the track.
+ */
+ public void configure(String mimeType, int channelCount, int sampleRate,
+ @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException {
+ configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null);
+ }
+
+ /**
+ * Configures (or reconfigures) the audio track.
+ *
+ * @param mimeType The mime type.
+ * @param channelCount The number of channels.
+ * @param sampleRate The sample rate in Hz.
+ * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT},
+ * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
+ * {@link C#ENCODING_PCM_32BIT}.
+ * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
+ * suitable buffer size automatically.
+ * @param outputChannels A mapping from input to output channels that is applied to this track's
+ * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the
+ * input unchanged. Otherwise, the element at index {@code i} specifies index of the input
+ * channel to map to output channel {@code i} when preprocessing input buffers. After the
+ * map is applied the audio data will have {@code outputChannels.length} channels.
+ * @throws ConfigurationException If an error occurs configuring the track.
+ */
+ public void configure(String mimeType, int channelCount, int sampleRate,
+ @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels)
+ throws ConfigurationException {
+ boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
+ @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding;
+ boolean flush = false;
+ if (!passthrough) {
+ pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount);
+ channelMappingAudioProcessor.setChannelMap(outputChannels);
+ for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ try {
+ flush |= audioProcessor.configure(sampleRate, channelCount, encoding);
+ } catch (AudioProcessor.UnhandledFormatException e) {
+ throw new ConfigurationException(e);
+ }
+ if (audioProcessor.isActive()) {
+ channelCount = audioProcessor.getOutputChannelCount();
+ encoding = audioProcessor.getOutputEncoding();
+ }
+ }
+ if (flush) {
+ resetAudioProcessors();
+ }
+ }
+
+ int channelConfig;
+ switch (channelCount) {
+ case 1:
+ channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+ break;
+ case 2:
+ channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+ break;
+ case 3:
+ channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+ break;
+ case 4:
+ channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
+ break;
+ case 5:
+ channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+ break;
+ case 6:
+ channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+ break;
+ case 7:
+ channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
+ break;
+ case 8:
+ channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
+ break;
+ default:
+ throw new ConfigurationException("Unsupported channel count: " + channelCount);
+ }
+
+ // Workaround for overly strict channel configuration checks on nVidia Shield.
+ if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
+ switch (channelCount) {
+ case 7:
+ channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
+ break;
+ case 3:
+ case 5:
+ channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Workaround for Nexus Player not reporting support for mono passthrough.
+ // (See [Internal: b/34268671].)
+ if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
+ channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+ }
+
+ if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate
+ && this.channelConfig == channelConfig) {
+ // We already have an audio track with the correct sample rate, channel config and encoding.
+ return;
+ }
+
+ reset();
+
+ this.encoding = encoding;
+ this.passthrough = passthrough;
+ this.sampleRate = sampleRate;
+ this.channelConfig = channelConfig;
+ outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT;
+ outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount);
+
+ if (specifiedBufferSize != 0) {
+ bufferSize = specifiedBufferSize;
+ } else if (passthrough) {
+ // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
+ // account. [Internal: b/25181305]
+ if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
+ // AC-3 allows bitrates up to 640 kbit/s.
+ bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND);
+ } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ {
+ // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
+ bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
+ }
+ } else {
+ int minBufferSize =
+ android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
+ Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
+ int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
+ int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
+ int maxAppBufferSize = (int) Math.max(minBufferSize,
+ durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
+ bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
+ : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
+ : multipliedBufferSize;
+ }
+ bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize);
+
+ // The old playback parameters may no longer be applicable so try to reset them now.
+ setPlaybackParameters(playbackParameters);
+ }
+
+ private void resetAudioProcessors() {
+ ArrayList<AudioProcessor> newAudioProcessors = new ArrayList<>();
+ for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ if (audioProcessor.isActive()) {
+ newAudioProcessors.add(audioProcessor);
+ } else {
+ audioProcessor.flush();
+ }
+ }
+ int count = newAudioProcessors.size();
+ audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
+ outputBuffers = new ByteBuffer[count];
+ for (int i = 0; i < count; i++) {
+ AudioProcessor audioProcessor = audioProcessors[i];
+ audioProcessor.flush();
+ outputBuffers[i] = audioProcessor.getOutput();
+ }
+ }
+
+ private void initialize() throws InitializationException {
+ // If we're asynchronously releasing a previous audio track then we block until it has been
+ // released. This guarantees that we cannot end up in a state where we have multiple audio
+ // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
+ // the shared memory that's available for audio track buffers. This would in turn cause the
+ // initialization of the audio track to fail.
+ releasingConditionVariable.block();
+
+ if (tunneling) {
+ audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding,
+ bufferSize, audioSessionId);
+ } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+ audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+ outputEncoding, bufferSize, MODE_STREAM);
+ } else {
+ // Re-attach to the same audio session.
+ audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+ outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
+ }
+ checkAudioTrackInitialized();
+
+ int audioSessionId = audioTrack.getAudioSessionId();
+ if (enablePreV21AudioSessionWorkaround) {
+ if (Util.SDK_INT < 21) {
+ // The workaround creates an audio track with a two byte buffer on the same session, and
+ // does not release it until this object is released, which keeps the session active.
+ if (keepSessionIdAudioTrack != null
+ && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
+ releaseKeepSessionIdAudioTrack();
+ }
+ if (keepSessionIdAudioTrack == null) {
+ int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
+ int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+ @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
+ int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
+ keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
+ channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
+ }
+ }
+ }
+ if (this.audioSessionId != audioSessionId) {
+ this.audioSessionId = audioSessionId;
+ listener.onAudioSessionId(audioSessionId);
+ }
+
+ audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
+ setVolumeInternal();
+ hasData = false;
+ }
+
+ /**
+ * Starts or resumes playing audio if the audio track has been initialized.
+ */
+ public void play() {
+ playing = true;
+ if (isInitialized()) {
+ resumeSystemTimeUs = System.nanoTime() / 1000;
+ audioTrack.play();
+ }
+ }
+
+ /**
+ * Signals to the audio track that the next buffer is discontinuous with the previous buffer.
+ */
+ public void handleDiscontinuity() {
+ // Force resynchronization after a skipped buffer.
+ if (startMediaTimeState == START_IN_SYNC) {
+ startMediaTimeState = START_NEED_SYNC;
+ }
+ }
+
+ /**
+ * Attempts to process data from a {@link ByteBuffer}, starting from its current position and
+ * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the
+ * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if
+ * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset.
+ * <p>
+ * Returns whether the data was handled in full. If the data was not handled in full then the same
+ * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
+ * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
+ * {@link #configure(String, int, int, int, int)} that caused the track to be reset).
+ *
+ * @param buffer The buffer containing audio data.
+ * @param presentationTimeUs The presentation timestamp of the buffer in microseconds.
+ * @return Whether the buffer was handled fully.
+ * @throws InitializationException If an error occurs initializing the track.
+ * @throws WriteException If an error occurs writing the audio data.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
+ throws InitializationException, WriteException {
+ Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer);
+ if (!isInitialized()) {
+ initialize();
+ if (playing) {
+ play();
+ }
+ }
+
+ if (needsPassthroughWorkarounds()) {
+ // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
+ // buffer empties. See [Internal: b/18899620].
+ if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
+ // We force an underrun to pause the track, so don't notify the listener in this case.
+ hasData = false;
+ return false;
+ }
+
+ // A new AC-3 audio track's playback position continues to increase from the old track's
+ // position for a short time after is has been released. Avoid writing data until the playback
+ // head position actually returns to zero.
+ if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
+ && audioTrackUtil.getPlaybackHeadPosition() != 0) {
+ return false;
+ }
+ }
+
+ boolean hadData = hasData;
+ hasData = hasPendingData();
+ if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
+ long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
+ listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
+ }
+
+ if (inputBuffer == null) {
+ // We are seeing this buffer for the first time.
+ if (!buffer.hasRemaining()) {
+ // The buffer is empty.
+ return true;
+ }
+
+ if (passthrough && framesPerEncodedSample == 0) {
+ // If this is the first encoded sample, calculate the sample size in frames.
+ framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
+ }
+
+ if (drainingPlaybackParameters != null) {
+ if (!drainAudioProcessorsToEndOfStream()) {
+ // Don't process any more input until draining completes.
+ return false;
+ }
+ // Store the position and corresponding media time from which the parameters will apply.
+ playbackParametersCheckpoints.add(new PlaybackParametersCheckpoint(
+ drainingPlaybackParameters, Math.max(0, presentationTimeUs),
+ framesToDurationUs(getWrittenFrames())));
+ drainingPlaybackParameters = null;
+ // The audio processors have drained, so flush them. This will cause any active speed
+ // adjustment audio processor to start producing audio with the new parameters.
+ resetAudioProcessors();
+ }
+
+ if (startMediaTimeState == START_NOT_SET) {
+ startMediaTimeUs = Math.max(0, presentationTimeUs);
+ startMediaTimeState = START_IN_SYNC;
+ } else {
+ // Sanity check that presentationTimeUs is consistent with the expected value.
+ long expectedPresentationTimeUs = startMediaTimeUs
+ + framesToDurationUs(getSubmittedFrames());
+ if (startMediaTimeState == START_IN_SYNC
+ && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) {
+ Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got "
+ + presentationTimeUs + "]");
+ startMediaTimeState = START_NEED_SYNC;
+ }
+ if (startMediaTimeState == START_NEED_SYNC) {
+ // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the
+ // number of bytes submitted.
+ startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
+ startMediaTimeState = START_IN_SYNC;
+ listener.onPositionDiscontinuity();
+ }
+ }
+
+ if (passthrough) {
+ submittedEncodedFrames += framesPerEncodedSample;
+ } else {
+ submittedPcmBytes += buffer.remaining();
+ }
+
+ inputBuffer = buffer;
+ }
+
+ if (passthrough) {
+ // Passthrough buffers are not processed.
+ writeBuffer(inputBuffer, presentationTimeUs);
+ } else {
+ processBuffers(presentationTimeUs);
+ }
+
+ if (!inputBuffer.hasRemaining()) {
+ inputBuffer = null;
+ return true;
+ }
+ return false;
+ }
+
+ private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {
+ int count = audioProcessors.length;
+ int index = count;
+ while (index >= 0) {
+ ByteBuffer input = index > 0 ? outputBuffers[index - 1]
+ : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER);
+ if (index == count) {
+ writeBuffer(input, avSyncPresentationTimeUs);
+ } else {
+ AudioProcessor audioProcessor = audioProcessors[index];
+ audioProcessor.queueInput(input);
+ ByteBuffer output = audioProcessor.getOutput();
+ outputBuffers[index] = output;
+ if (output.hasRemaining()) {
+ // Handle the output as input to the next audio processor or the AudioTrack.
+ index++;
+ continue;
+ }
+ }
+
+ if (input.hasRemaining()) {
+ // The input wasn't consumed and no output was produced, so give up for now.
+ return;
+ }
+
+ // Get more input from upstream.
+ index--;
+ }
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs)
+ throws WriteException {
+ if (!buffer.hasRemaining()) {
+ return true;
+ }
+ if (outputBuffer != null) {
+ Assertions.checkArgument(outputBuffer == buffer);
+ } else {
+ outputBuffer = buffer;
+ if (Util.SDK_INT < 21) {
+ int bytesRemaining = buffer.remaining();
+ if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) {
+ preV21OutputBuffer = new byte[bytesRemaining];
+ }
+ int originalPosition = buffer.position();
+ buffer.get(preV21OutputBuffer, 0, bytesRemaining);
+ buffer.position(originalPosition);
+ preV21OutputBufferOffset = 0;
+ }
+ }
+ int bytesRemaining = buffer.remaining();
+ int bytesWritten = 0;
+ if (Util.SDK_INT < 21) { // passthrough == false
+ // Work out how many bytes we can write without the risk of blocking.
+ int bytesPending =
+ (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
+ int bytesToWrite = bufferSize - bytesPending;
+ if (bytesToWrite > 0) {
+ bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
+ bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
+ if (bytesWritten > 0) {
+ preV21OutputBufferOffset += bytesWritten;
+ buffer.position(buffer.position() + bytesWritten);
+ }
+ }
+ } else if (tunneling) {
+ Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
+ bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining,
+ avSyncPresentationTimeUs);
+ } else {
+ bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
+ }
+
+ lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
+
+ if (bytesWritten < 0) {
+ throw new WriteException(bytesWritten);
+ }
+
+ if (!passthrough) {
+ writtenPcmBytes += bytesWritten;
+ }
+ if (bytesWritten == bytesRemaining) {
+ if (passthrough) {
+ writtenEncodedFrames += framesPerEncodedSample;
+ }
+ outputBuffer = null;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended.
+ *
+ * @throws WriteException If an error occurs draining data to the track.
+ */
+ public void playToEndOfStream() throws WriteException {
+ if (handledEndOfStream || !isInitialized()) {
+ return;
+ }
+
+ if (drainAudioProcessorsToEndOfStream()) {
+ // The audio processors have drained, so drain the underlying audio track.
+ audioTrackUtil.handleEndOfStream(getWrittenFrames());
+ bytesUntilNextAvSync = 0;
+ handledEndOfStream = true;
+ }
+ }
+
+ private boolean drainAudioProcessorsToEndOfStream() throws WriteException {
+ boolean audioProcessorNeedsEndOfStream = false;
+ if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
+ drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0;
+ audioProcessorNeedsEndOfStream = true;
+ }
+ while (drainingAudioProcessorIndex < audioProcessors.length) {
+ AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex];
+ if (audioProcessorNeedsEndOfStream) {
+ audioProcessor.queueEndOfStream();
+ }
+ processBuffers(C.TIME_UNSET);
+ if (!audioProcessor.isEnded()) {
+ return false;
+ }
+ audioProcessorNeedsEndOfStream = true;
+ drainingAudioProcessorIndex++;
+ }
+
+ // Finish writing any remaining output to the track.
+ if (outputBuffer != null) {
+ writeBuffer(outputBuffer, C.TIME_UNSET);
+ if (outputBuffer != null) {
+ return false;
+ }
+ }
+ drainingAudioProcessorIndex = C.INDEX_UNSET;
+ return true;
+ }
+
+ /**
+ * Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been
+ * completely processed and played.
+ */
+ public boolean isEnded() {
+ return !isInitialized() || (handledEndOfStream && !hasPendingData());
+ }
+
+ /**
+ * Returns whether the audio track has more data pending that will be played back.
+ */
+ public boolean hasPendingData() {
+ return isInitialized()
+ && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition()
+ || overrideHasPendingData());
+ }
+
+ /**
+ * Attempts to set the playback parameters and returns the active playback parameters, which may
+ * differ from those passed in.
+ *
+ * @param playbackParameters The new playback parameters to attempt to set.
+ * @return The active playback parameters.
+ */
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ if (passthrough) {
+ // The playback parameters are always the default in passthrough mode.
+ this.playbackParameters = PlaybackParameters.DEFAULT;
+ return this.playbackParameters;
+ }
+ playbackParameters = new PlaybackParameters(
+ sonicAudioProcessor.setSpeed(playbackParameters.speed),
+ sonicAudioProcessor.setPitch(playbackParameters.pitch));
+ PlaybackParameters lastSetPlaybackParameters =
+ drainingPlaybackParameters != null ? drainingPlaybackParameters
+ : !playbackParametersCheckpoints.isEmpty()
+ ? playbackParametersCheckpoints.getLast().playbackParameters
+ : this.playbackParameters;
+ if (!playbackParameters.equals(lastSetPlaybackParameters)) {
+ if (isInitialized()) {
+ // Drain the audio processors so we can determine the frame position at which the new
+ // parameters apply.
+ drainingPlaybackParameters = playbackParameters;
+ } else {
+ this.playbackParameters = playbackParameters;
+ }
+ }
+ return this.playbackParameters;
+ }
+
+ /**
+ * Gets the {@link PlaybackParameters}.
+ */
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+ /**
+ * Sets the stream type for audio track. If the stream type has changed and if the audio track
+ * is not configured for use with tunneling, then the audio track is reset and the audio session
+ * id is cleared.
+ * <p>
+ * If the audio track is configured for use with tunneling then the stream type is ignored, the
+ * audio track is not reset and the audio session id is not cleared. The passed stream type will
+ * be used if the audio track is later re-configured into non-tunneled mode.
+ *
+ * @param streamType The {@link C.StreamType} to use for audio output.
+ */
+ public void setStreamType(@C.StreamType int streamType) {
+ if (this.streamType == streamType) {
+ return;
+ }
+ this.streamType = streamType;
+ if (tunneling) {
+ // The stream type is ignored in tunneling mode, so no need to reset.
+ return;
+ }
+ reset();
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ }
+
+ /**
+ * Sets the audio session id. The audio track is reset if the audio session id has changed.
+ */
+ public void setAudioSessionId(int audioSessionId) {
+ if (this.audioSessionId != audioSessionId) {
+ this.audioSessionId = audioSessionId;
+ reset();
+ }
+ }
+
+ /**
+ * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the
+ * audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
+ * <p>
+ * If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that
+ * audio processors do not output buffers with a different duration than their input, and buffer
+ * processors must produce output corresponding to their last input immediately after that input
+ * is queued.
+ *
+ * @param tunnelingAudioSessionId The audio session id to use.
+ * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
+ */
+ public void enableTunnelingV21(int tunnelingAudioSessionId) {
+ Assertions.checkState(Util.SDK_INT >= 21);
+ if (!tunneling || audioSessionId != tunnelingAudioSessionId) {
+ tunneling = true;
+ audioSessionId = tunnelingAudioSessionId;
+ reset();
+ }
+ }
+
+ /**
+ * Disables tunneling. If tunneling was previously enabled then the audio track is reset and the
+ * audio session id is cleared.
+ */
+ public void disableTunneling() {
+ if (tunneling) {
+ tunneling = false;
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ reset();
+ }
+ }
+
+ /**
+ * Sets the playback volume.
+ *
+ * @param volume A volume in the range [0.0, 1.0].
+ */
+ public void setVolume(float volume) {
+ if (this.volume != volume) {
+ this.volume = volume;
+ setVolumeInternal();
+ }
+ }
+
+ private void setVolumeInternal() {
+ if (!isInitialized()) {
+ // Do nothing.
+ } else if (Util.SDK_INT >= 21) {
+ setVolumeInternalV21(audioTrack, volume);
+ } else {
+ setVolumeInternalV3(audioTrack, volume);
+ }
+ }
+
+ /**
+ * Pauses playback.
+ */
+ public void pause() {
+ playing = false;
+ if (isInitialized()) {
+ resetSyncParams();
+ audioTrackUtil.pause();
+ }
+ }
+
+ /**
+ * Releases the underlying audio track asynchronously.
+ * <p>
+ * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been
+ * released, so it is safe to use the audio track immediately after a reset. The audio session may
+ * remain active until {@link #release()} is called.
+ */
+ public void reset() {
+ if (isInitialized()) {
+ submittedPcmBytes = 0;
+ submittedEncodedFrames = 0;
+ writtenPcmBytes = 0;
+ writtenEncodedFrames = 0;
+ framesPerEncodedSample = 0;
+ if (drainingPlaybackParameters != null) {
+ playbackParameters = drainingPlaybackParameters;
+ drainingPlaybackParameters = null;
+ } else if (!playbackParametersCheckpoints.isEmpty()) {
+ playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters;
+ }
+ playbackParametersCheckpoints.clear();
+ playbackParametersOffsetUs = 0;
+ playbackParametersPositionUs = 0;
+ inputBuffer = null;
+ outputBuffer = null;
+ for (int i = 0; i < audioProcessors.length; i++) {
+ AudioProcessor audioProcessor = audioProcessors[i];
+ audioProcessor.flush();
+ outputBuffers[i] = audioProcessor.getOutput();
+ }
+ handledEndOfStream = false;
+ drainingAudioProcessorIndex = C.INDEX_UNSET;
+ avSyncHeader = null;
+ bytesUntilNextAvSync = 0;
+ startMediaTimeState = START_NOT_SET;
+ latencyUs = 0;
+ resetSyncParams();
+ int playState = audioTrack.getPlayState();
+ if (playState == PLAYSTATE_PLAYING) {
+ audioTrack.pause();
+ }
+ // AudioTrack.release can take some time, so we call it on a background thread.
+ final android.media.AudioTrack toRelease = audioTrack;
+ audioTrack = null;
+ audioTrackUtil.reconfigure(null, false);
+ releasingConditionVariable.close();
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ toRelease.flush();
+ toRelease.release();
+ } finally {
+ releasingConditionVariable.open();
+ }
+ }
+ }.start();
+ }
+ }
+
+ /**
+ * Releases all resources associated with this instance.
+ */
+ public void release() {
+ reset();
+ releaseKeepSessionIdAudioTrack();
+ for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ audioProcessor.reset();
+ }
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ playing = false;
+ }
+
+ /**
+ * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}.
+ */
+ private void releaseKeepSessionIdAudioTrack() {
+ if (keepSessionIdAudioTrack == null) {
+ return;
+ }
+
+ // AudioTrack.release can take some time, so we call it on a background thread.
+ final android.media.AudioTrack toRelease = keepSessionIdAudioTrack;
+ keepSessionIdAudioTrack = null;
+ new Thread() {
+ @Override
+ public void run() {
+ toRelease.release();
+ }
+ }.start();
+ }
+
+ /**
+ * Returns whether {@link #getCurrentPositionUs} can return the current playback position.
+ */
+ private boolean hasCurrentPositionUs() {
+ return isInitialized() && startMediaTimeState != START_NOT_SET;
+ }
+
+ /**
+ * Returns the underlying audio track {@code positionUs} with any applicable speedup applied.
+ */
+ private long applySpeedup(long positionUs) {
+ while (!playbackParametersCheckpoints.isEmpty()
+ && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) {
+ // We are playing (or about to play) media with the new playback parameters, so update them.
+ PlaybackParametersCheckpoint checkpoint = playbackParametersCheckpoints.remove();
+ playbackParameters = checkpoint.playbackParameters;
+ playbackParametersPositionUs = checkpoint.positionUs;
+ playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs;
+ }
+
+ if (playbackParameters.speed == 1f) {
+ return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs;
+ }
+
+ if (playbackParametersCheckpoints.isEmpty()
+ && sonicAudioProcessor.getOutputByteCount() >= SONIC_MIN_BYTES_FOR_SPEEDUP) {
+ return playbackParametersOffsetUs
+ + Util.scaleLargeTimestamp(positionUs - playbackParametersPositionUs,
+ sonicAudioProcessor.getInputByteCount(), sonicAudioProcessor.getOutputByteCount());
+ }
+
+ // We are playing drained data at a previous playback speed, or don't have enough bytes to
+ // calculate an accurate speedup, so fall back to multiplying by the speed.
+ return playbackParametersOffsetUs
+ + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs));
+ }
+
+ /**
+ * Updates the audio track latency and playback position parameters.
+ */
+ private void maybeSampleSyncParams() {
+ long playbackPositionUs = audioTrackUtil.getPositionUs();
+ if (playbackPositionUs == 0) {
+ // The AudioTrack hasn't output anything yet.
+ return;
+ }
+ long systemClockUs = System.nanoTime() / 1000;
+ if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
+ // Take a new sample and update the smoothed offset between the system clock and the playhead.
+ playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs;
+ nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
+ if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
+ playheadOffsetCount++;
+ }
+ lastPlayheadSampleTimeUs = systemClockUs;
+ smoothedPlayheadOffsetUs = 0;
+ for (int i = 0; i < playheadOffsetCount; i++) {
+ smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
+ }
+ }
+
+ if (needsPassthroughWorkarounds()) {
+ // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on
+ // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353].
+ return;
+ }
+
+ if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
+ audioTimestampSet = audioTrackUtil.updateTimestamp();
+ if (audioTimestampSet) {
+ // Perform sanity checks on the timestamp.
+ long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
+ long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition();
+ if (audioTimestampUs < resumeSystemTimeUs) {
+ // The timestamp corresponds to a time before the track was most recently resumed.
+ audioTimestampSet = false;
+ } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+ // The timestamp time base is probably wrong.
+ String message = "Spurious audio timestamp (system clock mismatch): "
+ + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
+ + playbackPositionUs;
+ if (failOnSpuriousAudioTimestamp) {
+ throw new InvalidAudioTrackTimestampException(message);
+ }
+ Log.w(TAG, message);
+ audioTimestampSet = false;
+ } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs)
+ > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+ // The timestamp frame position is probably wrong.
+ String message = "Spurious audio timestamp (frame position mismatch): "
+ + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
+ + playbackPositionUs;
+ if (failOnSpuriousAudioTimestamp) {
+ throw new InvalidAudioTrackTimestampException(message);
+ }
+ Log.w(TAG, message);
+ audioTimestampSet = false;
+ }
+ }
+ if (getLatencyMethod != null && !passthrough) {
+ try {
+ // Compute the audio track latency, excluding the latency due to the buffer (leaving
+ // latency due to the mixer and audio hardware driver).
+ latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L
+ - bufferSizeUs;
+ // Sanity check that the latency is non-negative.
+ latencyUs = Math.max(latencyUs, 0);
+ // Sanity check that the latency isn't too large.
+ if (latencyUs > MAX_LATENCY_US) {
+ Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
+ latencyUs = 0;
+ }
+ } catch (Exception e) {
+ // The method existed, but doesn't work. Don't try again.
+ getLatencyMethod = null;
+ }
+ }
+ lastTimestampSampleTimeUs = systemClockUs;
+ }
+ }
+
+ /**
+ * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
+ * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
+ * exception is thrown.
+ *
+ * @throws InitializationException If {@link #audioTrack} has not been successfully initialized.
+ */
+ private void checkAudioTrackInitialized() throws InitializationException {
+ int state = audioTrack.getState();
+ if (state == STATE_INITIALIZED) {
+ return;
+ }
+ // The track is not successfully initialized. Release and null the track.
+ try {
+ audioTrack.release();
+ } catch (Exception e) {
+ // The track has already failed to initialize, so it wouldn't be that surprising if release
+ // were to fail too. Swallow the exception.
+ } finally {
+ audioTrack = null;
+ }
+
+ throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
+ }
+
+ private boolean isInitialized() {
+ return audioTrack != null;
+ }
+
+ private long framesToDurationUs(long frameCount) {
+ return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
+ }
+
+ private long durationUsToFrames(long durationUs) {
+ return (durationUs * sampleRate) / C.MICROS_PER_SECOND;
+ }
+
+ private long getSubmittedFrames() {
+ return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize);
+ }
+
+ private long getWrittenFrames() {
+ return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize);
+ }
+
+ private void resetSyncParams() {
+ smoothedPlayheadOffsetUs = 0;
+ playheadOffsetCount = 0;
+ nextPlayheadOffsetIndex = 0;
+ lastPlayheadSampleTimeUs = 0;
+ audioTimestampSet = false;
+ lastTimestampSampleTimeUs = 0;
+ }
+
+ /**
+ * Returns whether to work around problems with passthrough audio tracks.
+ * See [Internal: b/18899620, b/19187573, b/21145353].
+ */
+ private boolean needsPassthroughWorkarounds() {
+ return Util.SDK_INT < 23
+ && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3);
+ }
+
+ /**
+ * Returns whether the audio track should behave as though it has pending data. This is to work
+ * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we
+ * empty their buffers when paused. In this case, they should still behave as if they have
+ * pending data, otherwise writing will never resume.
+ */
+ private boolean overrideHasPendingData() {
+ return needsPassthroughWorkarounds()
+ && audioTrack.getPlayState() == PLAYSTATE_PAUSED
+ && audioTrack.getPlaybackHeadPosition() == 0;
+ }
+
+ /**
+ * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
+ */
+ @TargetApi(21)
+ private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
+ int channelConfig, int encoding, int bufferSize, int sessionId) {
+ AudioAttributes attributesBuilder = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
+ .build();
+ AudioFormat format = new AudioFormat.Builder()
+ .setChannelMask(channelConfig)
+ .setEncoding(encoding)
+ .setSampleRate(sampleRate)
+ .build();
+ return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
+ sessionId);
+ }
+
+ @C.Encoding
+ private static int getEncodingForMimeType(String mimeType) {
+ switch (mimeType) {
+ case MimeTypes.AUDIO_AC3:
+ return C.ENCODING_AC3;
+ case MimeTypes.AUDIO_E_AC3:
+ return C.ENCODING_E_AC3;
+ case MimeTypes.AUDIO_DTS:
+ return C.ENCODING_DTS;
+ case MimeTypes.AUDIO_DTS_HD:
+ return C.ENCODING_DTS_HD;
+ default:
+ return C.ENCODING_INVALID;
+ }
+ }
+
+ private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
+ if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {
+ return DtsUtil.parseDtsAudioSampleCount(buffer);
+ } else if (encoding == C.ENCODING_AC3) {
+ return Ac3Util.getAc3SyncframeAudioSampleCount();
+ } else if (encoding == C.ENCODING_E_AC3) {
+ return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
+ } else {
+ throw new IllegalStateException("Unexpected audio encoding: " + encoding);
+ }
+ }
+
+ @TargetApi(21)
+ private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer,
+ int size) {
+ return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);
+ }
+
+ @TargetApi(21)
+ private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
+ ByteBuffer buffer, int size, long presentationTimeUs) {
+ // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
+ // if (Util.SDK_INT >= 23) {
+ // // The underlying platform AudioTrack writes AV sync headers directly.
+ // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
+ // }
+ if (avSyncHeader == null) {
+ avSyncHeader = ByteBuffer.allocate(16);
+ avSyncHeader.order(ByteOrder.BIG_ENDIAN);
+ avSyncHeader.putInt(0x55550001);
+ }
+ if (bytesUntilNextAvSync == 0) {
+ avSyncHeader.putInt(4, size);
+ avSyncHeader.putLong(8, presentationTimeUs * 1000);
+ avSyncHeader.position(0);
+ bytesUntilNextAvSync = size;
+ }
+ int avSyncHeaderBytesRemaining = avSyncHeader.remaining();
+ if (avSyncHeaderBytesRemaining > 0) {
+ int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING);
+ if (result < 0) {
+ bytesUntilNextAvSync = 0;
+ return result;
+ }
+ if (result < avSyncHeaderBytesRemaining) {
+ return 0;
+ }
+ }
+ int result = writeNonBlockingV21(audioTrack, buffer, size);
+ if (result < 0) {
+ bytesUntilNextAvSync = 0;
+ return result;
+ }
+ bytesUntilNextAvSync -= result;
+ return result;
+ }
+
+ @TargetApi(21)
+ private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) {
+ audioTrack.setVolume(volume);
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) {
+ audioTrack.setStereoVolume(volume, volume);
+ }
+
+ /**
+ * Wraps an {@link android.media.AudioTrack} to expose useful utility methods.
+ */
+ private static class AudioTrackUtil {
+
+ protected android.media.AudioTrack audioTrack;
+ private boolean needsPassthroughWorkaround;
+ private int sampleRate;
+ private long lastRawPlaybackHeadPosition;
+ private long rawPlaybackHeadWrapCount;
+ private long passthroughWorkaroundPauseOffset;
+
+ private long stopTimestampUs;
+ private long stopPlaybackHeadPosition;
+ private long endPlaybackHeadPosition;
+
+ /**
+ * Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
+ *
+ * @param audioTrack The audio track to wrap.
+ * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough
+ * audio tracks on platform API version 21/22.
+ */
+ public void reconfigure(android.media.AudioTrack audioTrack,
+ boolean needsPassthroughWorkaround) {
+ this.audioTrack = audioTrack;
+ this.needsPassthroughWorkaround = needsPassthroughWorkaround;
+ stopTimestampUs = C.TIME_UNSET;
+ lastRawPlaybackHeadPosition = 0;
+ rawPlaybackHeadWrapCount = 0;
+ passthroughWorkaroundPauseOffset = 0;
+ if (audioTrack != null) {
+ sampleRate = audioTrack.getSampleRate();
+ }
+ }
+
+ /**
+ * Stops the audio track in a way that ensures media written to it is played out in full, and
+ * that {@link #getPlaybackHeadPosition()} and {@link #getPositionUs()} continue to increment as
+ * the remaining media is played out.
+ *
+ * @param writtenFrames The total number of frames that have been written.
+ */
+ public void handleEndOfStream(long writtenFrames) {
+ stopPlaybackHeadPosition = getPlaybackHeadPosition();
+ stopTimestampUs = SystemClock.elapsedRealtime() * 1000;
+ endPlaybackHeadPosition = writtenFrames;
+ audioTrack.stop();
+ }
+
+ /**
+ * Pauses the audio track unless the end of the stream has been handled, in which case calling
+ * this method does nothing.
+ */
+ public void pause() {
+ if (stopTimestampUs != C.TIME_UNSET) {
+ // We don't want to knock the audio track back into the paused state.
+ return;
+ }
+ audioTrack.pause();
+ }
+
+ /**
+ * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be
+ * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method
+ * returns the playback head position as a long that will only wrap around if the value exceeds
+ * {@link Long#MAX_VALUE} (which in practice will never happen).
+ *
+ * @return The playback head position, in frames.
+ */
+ public long getPlaybackHeadPosition() {
+ if (stopTimestampUs != C.TIME_UNSET) {
+ // Simulate the playback head position up to the total number of frames submitted.
+ long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs;
+ long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND;
+ return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);
+ }
+
+ int state = audioTrack.getPlayState();
+ if (state == PLAYSTATE_STOPPED) {
+ // The audio track hasn't been started.
+ return 0;
+ }
+
+ long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
+ if (needsPassthroughWorkaround) {
+ // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22
+ // where the playback head position jumps back to zero on paused passthrough/direct audio
+ // tracks. See [Internal: b/19187573].
+ if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {
+ passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
+ }
+ rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
+ }
+ if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
+ // The value must have wrapped around.
+ rawPlaybackHeadWrapCount++;
+ }
+ lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
+ return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
+ }
+
+ /**
+ * Returns the duration of played media since reconfiguration, in microseconds.
+ */
+ public long getPositionUs() {
+ return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate;
+ }
+
+ /**
+ * Updates the values returned by {@link #getTimestampNanoTime()} and
+ * {@link #getTimestampFramePosition()}.
+ *
+ * @return Whether the timestamp values were updated.
+ */
+ public boolean updateTimestamp() {
+ return false;
+ }
+
+ /**
+ * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent
+ * call to {@link #updateTimestamp()} that returned true.
+ *
+ * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that
+ * returned true.
+ * @throws UnsupportedOperationException If the implementation does not support audio timestamp
+ * queries. {@link #updateTimestamp()} will always return false in this case.
+ */
+ public long getTimestampNanoTime() {
+ // Should never be called if updateTimestamp() returned false.
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most
+ * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that
+ * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will
+ * never happen).
+ *
+ * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()}
+ * that returned true.
+ * @throws UnsupportedOperationException If the implementation does not support audio timestamp
+ * queries. {@link #updateTimestamp()} will always return false in this case.
+ */
+ public long getTimestampFramePosition() {
+ // Should never be called if updateTimestamp() returned false.
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ @TargetApi(19)
+ private static class AudioTrackUtilV19 extends AudioTrackUtil {
+
+ private final AudioTimestamp audioTimestamp;
+
+ private long rawTimestampFramePositionWrapCount;
+ private long lastRawTimestampFramePosition;
+ private long lastTimestampFramePosition;
+
+ public AudioTrackUtilV19() {
+ audioTimestamp = new AudioTimestamp();
+ }
+
+ @Override
+ public void reconfigure(android.media.AudioTrack audioTrack,
+ boolean needsPassthroughWorkaround) {
+ super.reconfigure(audioTrack, needsPassthroughWorkaround);
+ rawTimestampFramePositionWrapCount = 0;
+ lastRawTimestampFramePosition = 0;
+ lastTimestampFramePosition = 0;
+ }
+
+ @Override
+ public boolean updateTimestamp() {
+ boolean updated = audioTrack.getTimestamp(audioTimestamp);
+ if (updated) {
+ long rawFramePosition = audioTimestamp.framePosition;
+ if (lastRawTimestampFramePosition > rawFramePosition) {
+ // The value must have wrapped around.
+ rawTimestampFramePositionWrapCount++;
+ }
+ lastRawTimestampFramePosition = rawFramePosition;
+ lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32);
+ }
+ return updated;
+ }
+
+ @Override
+ public long getTimestampNanoTime() {
+ return audioTimestamp.nanoTime;
+ }
+
+ @Override
+ public long getTimestampFramePosition() {
+ return lastTimestampFramePosition;
+ }
+
+ }
+
+ /**
+ * Stores playback parameters with the position and media time at which they apply.
+ */
+ private static final class PlaybackParametersCheckpoint {
+
+ private final PlaybackParameters playbackParameters;
+ private final long mediaTimeUs;
+ private final long positionUs;
+
+ private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs,
+ long positionUs) {
+ this.playbackParameters = playbackParameters;
+ this.mediaTimeUs = mediaTimeUs;
+ this.positionUs = positionUs;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
@@ -0,0 +1,162 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.C.Encoding;
+import com.google.android.exoplayer2.Format;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * An {@link AudioProcessor} that applies a mapping from input channels onto specified output
+ * channels. This can be used to reorder, duplicate or discard channels.
+ */
+/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor {
+
+ private int channelCount;
+ private int sampleRateHz;
+ private int[] pendingOutputChannels;
+
+ private boolean active;
+ private int[] outputChannels;
+ private ByteBuffer buffer;
+ private ByteBuffer outputBuffer;
+ private boolean inputEnded;
+
+ /**
+ * Creates a new processor that applies a channel mapping.
+ */
+ public ChannelMappingAudioProcessor() {
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ }
+
+ /**
+ * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
+ * to start using the new channel map.
+ *
+ * @see AudioTrack#configure(String, int, int, int, int, int[])
+ */
+ public void setChannelMap(int[] outputChannels) {
+ pendingOutputChannels = outputChannels;
+ }
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
+ throws UnhandledFormatException {
+ boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);
+ outputChannels = pendingOutputChannels;
+ if (outputChannels == null) {
+ active = false;
+ return outputChannelsChanged;
+ }
+ if (encoding != C.ENCODING_PCM_16BIT) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz
+ && this.channelCount == channelCount) {
+ return false;
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+
+ active = channelCount != outputChannels.length;
+ for (int i = 0; i < outputChannels.length; i++) {
+ int channelIndex = outputChannels[i];
+ if (channelIndex >= channelCount) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ active |= (channelIndex != i);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isActive() {
+ return active;
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return outputChannels == null ? channelCount : outputChannels.length;
+ }
+
+ @Override
+ public int getOutputEncoding() {
+ return C.ENCODING_PCM_16BIT;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ int position = inputBuffer.position();
+ int limit = inputBuffer.limit();
+ int frameCount = (limit - position) / (2 * channelCount);
+ int outputSize = frameCount * outputChannels.length * 2;
+ if (buffer.capacity() < outputSize) {
+ buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
+ } else {
+ buffer.clear();
+ }
+ while (position < limit) {
+ for (int channelIndex : outputChannels) {
+ buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
+ }
+ position += channelCount * 2;
+ }
+ inputBuffer.position(limit);
+ buffer.flip();
+ outputBuffer = buffer;
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public boolean isEnded() {
+ return inputEnded && outputBuffer == EMPTY_BUFFER;
+ }
+
+ @Override
+ public void flush() {
+ outputBuffer = EMPTY_BUFFER;
+ inputEnded = false;
+ }
+
+ @Override
+ public void reset() {
+ flush();
+ buffer = EMPTY_BUFFER;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ outputChannels = null;
+ active = false;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/DtsUtil.java
@@ -0,0 +1,116 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for parsing DTS frames.
+ */
+public final class DtsUtil {
+
+ /**
+ * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4.
+ */
+ private static final int[] CHANNELS_BY_AMODE = new int[] {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6,
+ 7, 8, 8};
+
+ /**
+ * Maps SFREQ to the sampling frequency in Hz. See ETSI TS 102 144 table 5.5.
+ */
+ private static final int[] SAMPLE_RATE_BY_SFREQ = new int[] {-1, 8000, 16000, 32000, -1, -1,
+ 11025, 22050, 44100, -1, -1, 12000, 24000, 48000, -1, -1};
+
+ /**
+ * Maps RATE to 2 * bitrate in kbit/s. See ETSI TS 102 144 table 5.7.
+ */
+ private static final int[] TWICE_BITRATE_KBPS_BY_RATE = new int[] {64, 112, 128, 192, 224, 256,
+ 384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816,
+ 2823, 2944, 3072, 3840, 4096, 6144, 7680};
+
+ /**
+ * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114
+ * subsections 5.3/5.4.
+ *
+ * @param frame The DTS frame to parse.
+ * @param trackId The track identifier to set on the format, or null.
+ * @param language The language to set on the format.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @return The DTS format parsed from data in the header.
+ */
+ public static Format parseDtsFormat(byte[] frame, String trackId, String language,
+ DrmInitData drmInitData) {
+ ParsableBitArray frameBits = new ParsableBitArray(frame);
+ frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
+ int amode = frameBits.readBits(6);
+ int channelCount = CHANNELS_BY_AMODE[amode];
+ int sfreq = frameBits.readBits(4);
+ int sampleRate = SAMPLE_RATE_BY_SFREQ[sfreq];
+ int rate = frameBits.readBits(5);
+ int bitrate = rate >= TWICE_BITRATE_KBPS_BY_RATE.length ? Format.NO_VALUE
+ : TWICE_BITRATE_KBPS_BY_RATE[rate] * 1000 / 2;
+ frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF
+ channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF
+ return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_DTS, null, bitrate,
+ Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+ }
+
+ /**
+ * Returns the number of audio samples represented by the given DTS frame.
+ *
+ * @param data The frame to parse.
+ * @return The number of audio samples represented by the frame.
+ */
+ public static int parseDtsAudioSampleCount(byte[] data) {
+ // See ETSI TS 102 114 subsection 5.4.1.
+ int nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2);
+ return (nblks + 1) * 32;
+ }
+
+ /**
+ * Like {@link #parseDtsAudioSampleCount(byte[])} but reads from a {@link ByteBuffer}. The
+ * buffer's position is not modified.
+ *
+ * @param buffer The {@link ByteBuffer} from which to read.
+ * @return The number of audio samples represented by the syncframe.
+ */
+ public static int parseDtsAudioSampleCount(ByteBuffer buffer) {
+ // See ETSI TS 102 114 subsection 5.4.1.
+ int position = buffer.position();
+ int nblks = ((buffer.get(position + 4) & 0x01) << 6)
+ | ((buffer.get(position + 5) & 0xFC) >> 2);
+ return (nblks + 1) * 32;
+ }
+
+ /**
+ * Returns the size in bytes of the given DTS frame.
+ *
+ * @param data The frame to parse.
+ * @return The frame's size in bytes.
+ */
+ public static int getDtsFrameSize(byte[] data) {
+ return (((data[5] & 0x02) << 12)
+ | ((data[6] & 0xFF) << 4)
+ | ((data[7] & 0xF0) >> 4)) + 1;
+ }
+
+ private DtsUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -0,0 +1,449 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.media.audiofx.Virtualizer;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
+ */
+@TargetApi(16)
+public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
+
+ private final EventDispatcher eventDispatcher;
+ private final AudioTrack audioTrack;
+
+ private boolean passthroughEnabled;
+ private boolean codecNeedsDiscardChannelsWorkaround;
+ private android.media.MediaFormat passthroughMediaFormat;
+ private int pcmEncoding;
+ private int channelCount;
+ private long currentPositionUs;
+ private boolean allowPositionDiscontinuity;
+
+ /**
+ * @param mediaCodecSelector A decoder selector.
+ */
+ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector) {
+ this(mediaCodecSelector, null, true);
+ }
+
+ /**
+ * @param mediaCodecSelector A decoder selector.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ */
+ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys) {
+ this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
+ }
+
+ /**
+ * @param mediaCodecSelector A decoder selector.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler,
+ AudioRendererEventListener eventListener) {
+ this(mediaCodecSelector, null, true, eventHandler, eventListener);
+ }
+
+ /**
+ * @param mediaCodecSelector A decoder selector.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys, Handler eventHandler,
+ AudioRendererEventListener eventListener) {
+ this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
+ eventListener, null);
+ }
+
+ /**
+ * @param mediaCodecSelector A decoder selector.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
+ * output.
+ */
+ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys, Handler eventHandler,
+ AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
+ AudioProcessor... audioProcessors) {
+ super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
+ audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ }
+
+ @Override
+ protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
+ throws DecoderQueryException {
+ String mimeType = format.sampleMimeType;
+ if (!MimeTypes.isAudio(mimeType)) {
+ return FORMAT_UNSUPPORTED_TYPE;
+ }
+ int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+ if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
+ return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
+ }
+ MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
+ if (decoderInfo == null) {
+ return FORMAT_UNSUPPORTED_SUBTYPE;
+ }
+ // Note: We assume support for unknown sampleRate and channelCount.
+ boolean decoderCapable = Util.SDK_INT < 21
+ || ((format.sampleRate == Format.NO_VALUE
+ || decoderInfo.isAudioSampleRateSupportedV21(format.sampleRate))
+ && (format.channelCount == Format.NO_VALUE
+ || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
+ int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
+ return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
+ }
+
+ @Override
+ protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
+ Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
+ if (allowPassthrough(format.sampleMimeType)) {
+ MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
+ if (passthroughDecoderInfo != null) {
+ passthroughEnabled = true;
+ return passthroughDecoderInfo;
+ }
+ }
+ passthroughEnabled = false;
+ return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder);
+ }
+
+ /**
+ * Returns whether encoded audio passthrough should be used for playing back the input format.
+ * This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that
+ * passthrough is supported.
+ *
+ * @param mimeType The type of input media.
+ * @return Whether passthrough playback should be used.
+ */
+ protected boolean allowPassthrough(String mimeType) {
+ return audioTrack.isPassthroughSupported(mimeType);
+ }
+
+ @Override
+ protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
+ MediaCrypto crypto) {
+ codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
+ if (passthroughEnabled) {
+ // Override the MIME type used to configure the codec if we are using a passthrough decoder.
+ passthroughMediaFormat = format.getFrameworkMediaFormatV16();
+ passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
+ codec.configure(passthroughMediaFormat, null, crypto, 0);
+ passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
+ } else {
+ codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0);
+ passthroughMediaFormat = null;
+ }
+ }
+
+ @Override
+ public MediaClock getMediaClock() {
+ return this;
+ }
+
+ @Override
+ protected void onCodecInitialized(String name, long initializedTimestampMs,
+ long initializationDurationMs) {
+ eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
+ }
+
+ @Override
+ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ super.onInputFormatChanged(newFormat);
+ eventDispatcher.inputFormatChanged(newFormat);
+ // If the input format is anything other than PCM then we assume that the audio decoder will
+ // output 16-bit PCM.
+ pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
+ : C.ENCODING_PCM_16BIT;
+ channelCount = newFormat.channelCount;
+ }
+
+ @Override
+ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
+ throws ExoPlaybackException {
+ boolean passthrough = passthroughMediaFormat != null;
+ String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
+ : MimeTypes.AUDIO_RAW;
+ MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
+ int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+ int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+ int[] channelMap;
+ if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) {
+ channelMap = new int[this.channelCount];
+ for (int i = 0; i < this.channelCount; i++) {
+ channelMap[i] = i;
+ }
+ } else {
+ channelMap = null;
+ }
+
+ try {
+ audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap);
+ } catch (AudioTrack.ConfigurationException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ /**
+ * Called when the audio session id becomes known. The default implementation is a no-op. One
+ * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
+ * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
+ * should be released in {@link #onDisabled()} (if not before).
+ *
+ * @see AudioTrack.Listener#onAudioSessionId(int)
+ */
+ protected void onAudioSessionId(int audioSessionId) {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioTrack.Listener#onPositionDiscontinuity()
+ */
+ protected void onAudioTrackPositionDiscontinuity() {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioTrack.Listener#onUnderrun(int, long, long)
+ */
+ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+ long elapsedSinceLastFeedMs) {
+ // Do nothing.
+ }
+
+ @Override
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ super.onEnabled(joining);
+ eventDispatcher.enabled(decoderCounters);
+ int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+ if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
+ } else {
+ audioTrack.disableTunneling();
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ super.onPositionReset(positionUs, joining);
+ audioTrack.reset();
+ currentPositionUs = positionUs;
+ allowPositionDiscontinuity = true;
+ }
+
+ @Override
+ protected void onStarted() {
+ super.onStarted();
+ audioTrack.play();
+ }
+
+ @Override
+ protected void onStopped() {
+ audioTrack.pause();
+ super.onStopped();
+ }
+
+ @Override
+ protected void onDisabled() {
+ try {
+ audioTrack.release();
+ } finally {
+ try {
+ super.onDisabled();
+ } finally {
+ decoderCounters.ensureUpdated();
+ eventDispatcher.disabled(decoderCounters);
+ }
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return super.isEnded() && audioTrack.isEnded();
+ }
+
+ @Override
+ public boolean isReady() {
+ return audioTrack.hasPendingData() || super.isReady();
+ }
+
+ @Override
+ public long getPositionUs() {
+ long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
+ if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) {
+ currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
+ : Math.max(currentPositionUs, newCurrentPositionUs);
+ allowPositionDiscontinuity = false;
+ }
+ return currentPositionUs;
+ }
+
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ return audioTrack.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return audioTrack.getPlaybackParameters();
+ }
+
+ @Override
+ protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
+ ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
+ boolean shouldSkip) throws ExoPlaybackException {
+ if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ // Discard output buffers from the passthrough (raw) decoder containing codec specific data.
+ codec.releaseOutputBuffer(bufferIndex, false);
+ return true;
+ }
+
+ if (shouldSkip) {
+ codec.releaseOutputBuffer(bufferIndex, false);
+ decoderCounters.skippedOutputBufferCount++;
+ audioTrack.handleDiscontinuity();
+ return true;
+ }
+
+ try {
+ if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
+ codec.releaseOutputBuffer(bufferIndex, false);
+ decoderCounters.renderedOutputBufferCount++;
+ return true;
+ }
+ } catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ return false;
+ }
+
+ @Override
+ protected void renderToEndOfStream() throws ExoPlaybackException {
+ try {
+ audioTrack.playToEndOfStream();
+ } catch (AudioTrack.WriteException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ switch (messageType) {
+ case C.MSG_SET_VOLUME:
+ audioTrack.setVolume((Float) message);
+ break;
+ case C.MSG_SET_STREAM_TYPE:
+ @C.StreamType int streamType = (Integer) message;
+ audioTrack.setStreamType(streamType);
+ break;
+ default:
+ super.handleMessage(messageType, message);
+ break;
+ }
+ }
+
+ /**
+ * Returns whether the decoder is known to output six audio channels when provided with input with
+ * fewer than six channels.
+ * <p>
+ * See [Internal: b/35655036].
+ */
+ private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {
+ // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.
+ return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName)
+ && "samsung".equals(Util.MANUFACTURER)
+ && (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte")
+ || Util.DEVICE.startsWith("heroqlte"));
+ }
+
+ private final class AudioTrackListener implements AudioTrack.Listener {
+
+ @Override
+ public void onAudioSessionId(int audioSessionId) {
+ eventDispatcher.audioSessionId(audioSessionId);
+ MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ onAudioTrackPositionDiscontinuity();
+ // We are out of sync so allow currentPositionUs to jump backwards.
+ MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
+ }
+
+ @Override
+ public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
@@ -0,0 +1,179 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}.
+ */
+/* package */ final class ResamplingAudioProcessor implements AudioProcessor {
+
+ private int sampleRateHz;
+ private int channelCount;
+ @C.PcmEncoding
+ private int encoding;
+ private ByteBuffer buffer;
+ private ByteBuffer outputBuffer;
+ private boolean inputEnded;
+
+ /**
+ * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
+ */
+ public ResamplingAudioProcessor() {
+ sampleRateHz = Format.NO_VALUE;
+ channelCount = Format.NO_VALUE;
+ encoding = C.ENCODING_INVALID;
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ }
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
+ throws UnhandledFormatException {
+ if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
+ && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount
+ && this.encoding == encoding) {
+ return false;
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ this.encoding = encoding;
+ if (encoding == C.ENCODING_PCM_16BIT) {
+ buffer = EMPTY_BUFFER;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isActive() {
+ return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT;
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return channelCount;
+ }
+
+ @Override
+ public int getOutputEncoding() {
+ return C.ENCODING_PCM_16BIT;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ // Prepare the output buffer.
+ int position = inputBuffer.position();
+ int limit = inputBuffer.limit();
+ int size = limit - position;
+ int resampledSize;
+ switch (encoding) {
+ case C.ENCODING_PCM_8BIT:
+ resampledSize = size * 2;
+ break;
+ case C.ENCODING_PCM_24BIT:
+ resampledSize = (size / 3) * 2;
+ break;
+ case C.ENCODING_PCM_32BIT:
+ resampledSize = size / 2;
+ break;
+ case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_INVALID:
+ case Format.NO_VALUE:
+ default:
+ throw new IllegalStateException();
+ }
+ if (buffer.capacity() < resampledSize) {
+ buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
+ } else {
+ buffer.clear();
+ }
+
+ // Resample the little endian input and update the input/output buffers.
+ switch (encoding) {
+ case C.ENCODING_PCM_8BIT:
+ // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
+ for (int i = position; i < limit; i++) {
+ buffer.put((byte) 0);
+ buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
+ }
+ break;
+ case C.ENCODING_PCM_24BIT:
+ // 24->16 bit resampling. Drop the least significant byte.
+ for (int i = position; i < limit; i += 3) {
+ buffer.put(inputBuffer.get(i + 1));
+ buffer.put(inputBuffer.get(i + 2));
+ }
+ break;
+ case C.ENCODING_PCM_32BIT:
+ // 32->16 bit resampling. Drop the two least significant bytes.
+ for (int i = position; i < limit; i += 4) {
+ buffer.put(inputBuffer.get(i + 2));
+ buffer.put(inputBuffer.get(i + 3));
+ }
+ break;
+ case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_INVALID:
+ case Format.NO_VALUE:
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ inputBuffer.position(inputBuffer.limit());
+ buffer.flip();
+ outputBuffer = buffer;
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public boolean isEnded() {
+ return inputEnded && outputBuffer == EMPTY_BUFFER;
+ }
+
+ @Override
+ public void flush() {
+ outputBuffer = EMPTY_BUFFER;
+ inputEnded = false;
+ }
+
+ @Override
+ public void reset() {
+ flush();
+ buffer = EMPTY_BUFFER;
+ sampleRateHz = Format.NO_VALUE;
+ channelCount = Format.NO_VALUE;
+ encoding = C.ENCODING_INVALID;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -0,0 +1,631 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import android.media.audiofx.Virtualizer;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
+import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
+import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Decodes and renders audio using a {@link SimpleDecoder}.
+ */
+public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
+ REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
+ private @interface ReinitializationState {}
+ /**
+ * The decoder does not need to be re-initialized.
+ */
+ private static final int REINITIALIZATION_STATE_NONE = 0;
+ /**
+ * The input format has changed in a way that requires the decoder to be re-initialized, but we
+ * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
+ * ensure that it outputs any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
+ /**
+ * The input format has changed in a way that requires the decoder to be re-initialized, and we've
+ * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
+ * end of stream signal to indicate that it has output any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
+
+ private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
+ private final boolean playClearSamplesWithoutKeys;
+ private final EventDispatcher eventDispatcher;
+ private final AudioTrack audioTrack;
+ private final FormatHolder formatHolder;
+ private final DecoderInputBuffer flagsOnlyBuffer;
+
+ private DecoderCounters decoderCounters;
+ private Format inputFormat;
+ private SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
+ ? extends AudioDecoderException> decoder;
+ private DecoderInputBuffer inputBuffer;
+ private SimpleOutputBuffer outputBuffer;
+ private DrmSession<ExoMediaCrypto> drmSession;
+ private DrmSession<ExoMediaCrypto> pendingDrmSession;
+
+ @ReinitializationState private int decoderReinitializationState;
+ private boolean decoderReceivedBuffers;
+ private boolean audioTrackNeedsConfigure;
+
+ private long currentPositionUs;
+ private boolean allowPositionDiscontinuity;
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+ private boolean waitingForKeys;
+
+ public SimpleDecoderAudioRenderer() {
+ this(null, null);
+ }
+
+ /**
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ */
+ public SimpleDecoderAudioRenderer(Handler eventHandler,
+ AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) {
+ this(eventHandler, eventListener, null, null, false, audioProcessors);
+ }
+
+ /**
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ */
+ public SimpleDecoderAudioRenderer(Handler eventHandler,
+ AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
+ this(eventHandler, eventListener, audioCapabilities, null, false);
+ }
+
+ /**
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+ * media is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ */
+ public SimpleDecoderAudioRenderer(Handler eventHandler,
+ AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
+ DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
+ AudioProcessor... audioProcessors) {
+ super(C.TRACK_TYPE_AUDIO);
+ this.drmSessionManager = drmSessionManager;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
+ formatHolder = new FormatHolder();
+ flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ audioTrackNeedsConfigure = true;
+ }
+
+ @Override
+ public MediaClock getMediaClock() {
+ return this;
+ }
+
+ @Override
+ public final int supportsFormat(Format format) {
+ int formatSupport = supportsFormatInternal(format);
+ if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) {
+ return formatSupport;
+ }
+ int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+ return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
+ }
+
+ /**
+ * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
+ * {@link #supportsFormat(Format)}.
+ *
+ * @param format The format.
+ * @return The extent to which the renderer supports the format itself.
+ */
+ protected abstract int supportsFormatInternal(Format format);
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (outputStreamEnded) {
+ try {
+ audioTrack.playToEndOfStream();
+ } catch (AudioTrack.WriteException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ return;
+ }
+
+ // Try and read a format if we don't have one already.
+ if (inputFormat == null) {
+ // We don't have a format yet, so try and read one.
+ flagsOnlyBuffer.clear();
+ int result = readSource(formatHolder, flagsOnlyBuffer, true);
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder.format);
+ } else if (result == C.RESULT_BUFFER_READ) {
+ // End of stream read having not read a format.
+ Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
+ inputStreamEnded = true;
+ processEndOfStream();
+ return;
+ } else {
+ // We still don't have a format and can't make progress without one.
+ return;
+ }
+ }
+
+ // If we don't have a decoder yet, we need to instantiate one.
+ maybeInitDecoder();
+
+ if (decoder != null) {
+ try {
+ // Rendering loop.
+ TraceUtil.beginSection("drainAndFeed");
+ while (drainOutputBuffer()) {}
+ while (feedInputBuffer()) {}
+ TraceUtil.endSection();
+ } catch (AudioDecoderException | AudioTrack.ConfigurationException
+ | AudioTrack.InitializationException | AudioTrack.WriteException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ decoderCounters.ensureUpdated();
+ }
+ }
+
+ /**
+ * Called when the audio session id becomes known. The default implementation is a no-op. One
+ * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
+ * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
+ * should be released in {@link #onDisabled()} (if not before).
+ *
+ * @see AudioTrack.Listener#onAudioSessionId(int)
+ */
+ protected void onAudioSessionId(int audioSessionId) {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioTrack.Listener#onPositionDiscontinuity()
+ */
+ protected void onAudioTrackPositionDiscontinuity() {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioTrack.Listener#onUnderrun(int, long, long)
+ */
+ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+ long elapsedSinceLastFeedMs) {
+ // Do nothing.
+ }
+
+ /**
+ * Creates a decoder for the given format.
+ *
+ * @param format The format for which a decoder is required.
+ * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content.
+ * Maybe null and can be ignored if decoder does not handle encrypted content.
+ * @return The decoder.
+ * @throws AudioDecoderException If an error occurred creating a suitable decoder.
+ */
+ protected abstract SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
+ ? extends AudioDecoderException> createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ throws AudioDecoderException;
+
+ /**
+ * Returns the format of audio buffers output by the decoder. Will not be called until the first
+ * output buffer has been dequeued, so the decoder may use input data to determine the format.
+ * <p>
+ * The default implementation returns a 16-bit PCM format with the same channel count and sample
+ * rate as the input.
+ */
+ protected Format getOutputFormat() {
+ return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
+ Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT,
+ null, null, 0, null);
+ }
+
+ private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
+ AudioTrack.ConfigurationException, AudioTrack.InitializationException,
+ AudioTrack.WriteException {
+ if (outputBuffer == null) {
+ outputBuffer = decoder.dequeueOutputBuffer();
+ if (outputBuffer == null) {
+ return false;
+ }
+ decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
+ }
+
+ if (outputBuffer.isEndOfStream()) {
+ if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+ // We're waiting to re-initialize the decoder, and have now processed all final buffers.
+ releaseDecoder();
+ maybeInitDecoder();
+ // The audio track may need to be recreated once the new output format is known.
+ audioTrackNeedsConfigure = true;
+ } else {
+ outputBuffer.release();
+ outputBuffer = null;
+ processEndOfStream();
+ }
+ return false;
+ }
+
+ if (audioTrackNeedsConfigure) {
+ Format outputFormat = getOutputFormat();
+ audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount,
+ outputFormat.sampleRate, outputFormat.pcmEncoding, 0);
+ audioTrackNeedsConfigure = false;
+ }
+
+ if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
+ decoderCounters.renderedOutputBufferCount++;
+ outputBuffer.release();
+ outputBuffer = null;
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException {
+ if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ || inputStreamEnded) {
+ // We need to reinitialize the decoder or the input stream has ended.
+ return false;
+ }
+
+ if (inputBuffer == null) {
+ inputBuffer = decoder.dequeueInputBuffer();
+ if (inputBuffer == null) {
+ return false;
+ }
+ }
+
+ if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+ inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
+ int result;
+ if (waitingForKeys) {
+ // We've already read an encrypted sample into buffer, and are waiting for keys.
+ result = C.RESULT_BUFFER_READ;
+ } else {
+ result = readSource(formatHolder, inputBuffer, false);
+ }
+
+ if (result == C.RESULT_NOTHING_READ) {
+ return false;
+ }
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder.format);
+ return true;
+ }
+ if (inputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ return false;
+ }
+ boolean bufferEncrypted = inputBuffer.isEncrypted();
+ waitingForKeys = shouldWaitForKeys(bufferEncrypted);
+ if (waitingForKeys) {
+ return false;
+ }
+ inputBuffer.flip();
+ decoder.queueInputBuffer(inputBuffer);
+ decoderReceivedBuffers = true;
+ decoderCounters.inputBufferCount++;
+ inputBuffer = null;
+ return true;
+ }
+
+ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+ if (drmSession == null) {
+ return false;
+ }
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ }
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
+ && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ }
+
+ private void processEndOfStream() throws ExoPlaybackException {
+ outputStreamEnded = true;
+ try {
+ audioTrack.playToEndOfStream();
+ } catch (AudioTrack.WriteException e) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ }
+ }
+
+ private void flushDecoder() throws ExoPlaybackException {
+ waitingForKeys = false;
+ if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ inputBuffer = null;
+ if (outputBuffer != null) {
+ outputBuffer.release();
+ outputBuffer = null;
+ }
+ decoder.flush();
+ decoderReceivedBuffers = false;
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded && audioTrack.isEnded();
+ }
+
+ @Override
+ public boolean isReady() {
+ return audioTrack.hasPendingData()
+ || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));
+ }
+
+ @Override
+ public long getPositionUs() {
+ long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
+ if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) {
+ currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
+ : Math.max(currentPositionUs, newCurrentPositionUs);
+ allowPositionDiscontinuity = false;
+ }
+ return currentPositionUs;
+ }
+
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ return audioTrack.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return audioTrack.getPlaybackParameters();
+ }
+
+ @Override
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ decoderCounters = new DecoderCounters();
+ eventDispatcher.enabled(decoderCounters);
+ int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+ if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
+ } else {
+ audioTrack.disableTunneling();
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ audioTrack.reset();
+ currentPositionUs = positionUs;
+ allowPositionDiscontinuity = true;
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ if (decoder != null) {
+ flushDecoder();
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ audioTrack.play();
+ }
+
+ @Override
+ protected void onStopped() {
+ audioTrack.pause();
+ }
+
+ @Override
+ protected void onDisabled() {
+ inputFormat = null;
+ audioTrackNeedsConfigure = true;
+ waitingForKeys = false;
+ try {
+ releaseDecoder();
+ audioTrack.release();
+ } finally {
+ try {
+ if (drmSession != null) {
+ drmSessionManager.releaseSession(drmSession);
+ }
+ } finally {
+ try {
+ if (pendingDrmSession != null && pendingDrmSession != drmSession) {
+ drmSessionManager.releaseSession(pendingDrmSession);
+ }
+ } finally {
+ drmSession = null;
+ pendingDrmSession = null;
+ decoderCounters.ensureUpdated();
+ eventDispatcher.disabled(decoderCounters);
+ }
+ }
+ }
+ }
+
+ private void maybeInitDecoder() throws ExoPlaybackException {
+ if (decoder != null) {
+ return;
+ }
+
+ drmSession = pendingDrmSession;
+ ExoMediaCrypto mediaCrypto = null;
+ if (drmSession != null) {
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ } else if (drmSessionState == DrmSession.STATE_OPENED
+ || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
+ mediaCrypto = drmSession.getMediaCrypto();
+ } else {
+ // The drm session isn't open yet.
+ return;
+ }
+ }
+
+ try {
+ long codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createAudioDecoder");
+ decoder = createDecoder(inputFormat, mediaCrypto);
+ TraceUtil.endSection();
+ long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
+ codecInitializedTimestamp - codecInitializingTimestamp);
+ decoderCounters.decoderInitCount++;
+ } catch (AudioDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ private void releaseDecoder() {
+ if (decoder == null) {
+ return;
+ }
+
+ inputBuffer = null;
+ outputBuffer = null;
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ decoderReceivedBuffers = false;
+ }
+
+ private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ Format oldFormat = inputFormat;
+ inputFormat = newFormat;
+
+ boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null
+ : oldFormat.drmInitData);
+ if (drmInitDataChanged) {
+ if (inputFormat.drmInitData != null) {
+ if (drmSessionManager == null) {
+ throw ExoPlaybackException.createForRenderer(
+ new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+ }
+ pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
+ inputFormat.drmInitData);
+ if (pendingDrmSession == drmSession) {
+ drmSessionManager.releaseSession(pendingDrmSession);
+ }
+ } else {
+ pendingDrmSession = null;
+ }
+ }
+
+ if (decoderReceivedBuffers) {
+ // Signal end of stream and wait for any final output buffers before re-initialization.
+ decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ // There aren't any final output buffers, so release the decoder immediately.
+ releaseDecoder();
+ maybeInitDecoder();
+ audioTrackNeedsConfigure = true;
+ }
+
+ eventDispatcher.inputFormatChanged(newFormat);
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ switch (messageType) {
+ case C.MSG_SET_VOLUME:
+ audioTrack.setVolume((Float) message);
+ break;
+ case C.MSG_SET_STREAM_TYPE:
+ @C.StreamType int streamType = (Integer) message;
+ audioTrack.setStreamType(streamType);
+ break;
+ default:
+ super.handleMessage(messageType, message);
+ break;
+ }
+ }
+
+ private final class AudioTrackListener implements AudioTrack.Listener {
+
+ @Override
+ public void onAudioSessionId(int audioSessionId) {
+ eventDispatcher.audioSessionId(audioSessionId);
+ SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ onAudioTrackPositionDiscontinuity();
+ // We are out of sync so allow currentPositionUs to jump backwards.
+ SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
+ }
+
+ @Override
+ public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/Sonic.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2010 Bill Cox, Sonic Library
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.util.Assertions;
+import java.nio.ShortBuffer;
+import java.util.Arrays;
+
+/**
+ * Sonic audio stream processor for time/pitch stretching.
+ * <p>
+ * Based on https://github.com/waywardgeek/sonic.
+ */
+/* package */ final class Sonic {
+
+ private static final boolean USE_CHORD_PITCH = false;
+ private static final int MINIMUM_PITCH = 65;
+ private static final int MAXIMUM_PITCH = 400;
+ private static final int AMDF_FREQUENCY = 4000;
+
+ private final int sampleRate;
+ private final int numChannels;
+ private final int minPeriod;
+ private final int maxPeriod;
+ private final int maxRequired;
+ private final short[] downSampleBuffer;
+
+ private int inputBufferSize;
+ private short[] inputBuffer;
+ private int outputBufferSize;
+ private short[] outputBuffer;
+ private int pitchBufferSize;
+ private short[] pitchBuffer;
+ private int oldRatePosition;
+ private int newRatePosition;
+ private float speed;
+ private float pitch;
+ private int numInputSamples;
+ private int numOutputSamples;
+ private int numPitchSamples;
+ private int remainingInputToCopy;
+ private int prevPeriod;
+ private int prevMinDiff;
+ private int minDiff;
+ private int maxDiff;
+
+ /**
+ * Creates a new Sonic audio stream processor.
+ *
+ * @param sampleRate The sample rate of input audio.
+ * @param numChannels The number of channels in the input audio.
+ */
+ public Sonic(int sampleRate, int numChannels) {
+ this.sampleRate = sampleRate;
+ this.numChannels = numChannels;
+ minPeriod = sampleRate / MAXIMUM_PITCH;
+ maxPeriod = sampleRate / MINIMUM_PITCH;
+ maxRequired = 2 * maxPeriod;
+ downSampleBuffer = new short[maxRequired];
+ inputBufferSize = maxRequired;
+ inputBuffer = new short[maxRequired * numChannels];
+ outputBufferSize = maxRequired;
+ outputBuffer = new short[maxRequired * numChannels];
+ pitchBufferSize = maxRequired;
+ pitchBuffer = new short[maxRequired * numChannels];
+ oldRatePosition = 0;
+ newRatePosition = 0;
+ prevPeriod = 0;
+ speed = 1.0f;
+ pitch = 1.0f;
+ }
+
+ /**
+ * Sets the output speed.
+ */
+ public void setSpeed(float speed) {
+ this.speed = speed;
+ }
+
+ /**
+ * Gets the output speed.
+ */
+ public float getSpeed() {
+ return speed;
+ }
+
+ /**
+ * Sets the output pitch.
+ */
+ public void setPitch(float pitch) {
+ this.pitch = pitch;
+ }
+
+ /**
+ * Gets the output pitch.
+ */
+ public float getPitch() {
+ return pitch;
+ }
+
+ /**
+ * Queues remaining data from {@code buffer}, and advances its position by the number of bytes
+ * consumed.
+ *
+ * @param buffer A {@link ShortBuffer} containing input data between its position and limit.
+ */
+ public void queueInput(ShortBuffer buffer) {
+ int samplesToWrite = buffer.remaining() / numChannels;
+ int bytesToWrite = samplesToWrite * numChannels * 2;
+ enlargeInputBufferIfNeeded(samplesToWrite);
+ buffer.get(inputBuffer, numInputSamples * numChannels, bytesToWrite / 2);
+ numInputSamples += samplesToWrite;
+ processStreamInput();
+ }
+
+ /**
+ * Gets available output, outputting to the start of {@code buffer}. The buffer's position will be
+ * advanced by the number of bytes written.
+ *
+ * @param buffer A {@link ShortBuffer} into which output will be written.
+ */
+ public void getOutput(ShortBuffer buffer) {
+ int samplesToRead = Math.min(buffer.remaining() / numChannels, numOutputSamples);
+ buffer.put(outputBuffer, 0, samplesToRead * numChannels);
+ numOutputSamples -= samplesToRead;
+ System.arraycopy(outputBuffer, samplesToRead * numChannels, outputBuffer, 0,
+ numOutputSamples * numChannels);
+ }
+
+ /**
+ * Forces generating output using whatever data has been queued already. No extra delay will be
+ * added to the output, but flushing in the middle of words could introduce distortion.
+ */
+ public void queueEndOfStream() {
+ int remainingSamples = numInputSamples;
+ float s = speed / pitch;
+ int expectedOutputSamples =
+ numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / pitch + 0.5f);
+
+ // Add enough silence to flush both input and pitch buffers.
+ enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired);
+ for (int xSample = 0; xSample < 2 * maxRequired * numChannels; xSample++) {
+ inputBuffer[remainingSamples * numChannels + xSample] = 0;
+ }
+ numInputSamples += 2 * maxRequired;
+ processStreamInput();
+ // Throw away any extra samples we generated due to the silence we added.
+ if (numOutputSamples > expectedOutputSamples) {
+ numOutputSamples = expectedOutputSamples;
+ }
+ // Empty input and pitch buffers.
+ numInputSamples = 0;
+ remainingInputToCopy = 0;
+ numPitchSamples = 0;
+ }
+
+ /**
+ * Returns the number of output samples that can be read with {@link #getOutput(ShortBuffer)}.
+ */
+ public int getSamplesAvailable() {
+ return numOutputSamples;
+ }
+
+ // Internal methods.
+
+ private void enlargeOutputBufferIfNeeded(int numSamples) {
+ if (numOutputSamples + numSamples > outputBufferSize) {
+ outputBufferSize += (outputBufferSize / 2) + numSamples;
+ outputBuffer = Arrays.copyOf(outputBuffer, outputBufferSize * numChannels);
+ }
+ }
+
+ private void enlargeInputBufferIfNeeded(int numSamples) {
+ if (numInputSamples + numSamples > inputBufferSize) {
+ inputBufferSize += (inputBufferSize / 2) + numSamples;
+ inputBuffer = Arrays.copyOf(inputBuffer, inputBufferSize * numChannels);
+ }
+ }
+
+ private void removeProcessedInputSamples(int position) {
+ int remainingSamples = numInputSamples - position;
+ System.arraycopy(inputBuffer, position * numChannels, inputBuffer, 0,
+ remainingSamples * numChannels);
+ numInputSamples = remainingSamples;
+ }
+
+ private void copyToOutput(short[] samples, int position, int numSamples) {
+ enlargeOutputBufferIfNeeded(numSamples);
+ System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels,
+ numSamples * numChannels);
+ numOutputSamples += numSamples;
+ }
+
+ private int copyInputToOutput(int position) {
+ int numSamples = Math.min(maxRequired, remainingInputToCopy);
+ copyToOutput(inputBuffer, position, numSamples);
+ remainingInputToCopy -= numSamples;
+ return numSamples;
+ }
+
+ private void downSampleInput(short[] samples, int position, int skip) {
+ // If skip is greater than one, average skip samples together and write them to the down-sample
+ // buffer. If numChannels is greater than one, mix the channels together as we down sample.
+ int numSamples = maxRequired / skip;
+ int samplesPerValue = numChannels * skip;
+ position *= numChannels;
+ for (int i = 0; i < numSamples; i++) {
+ int value = 0;
+ for (int j = 0; j < samplesPerValue; j++) {
+ value += samples[position + i * samplesPerValue + j];
+ }
+ value /= samplesPerValue;
+ downSampleBuffer[i] = (short) value;
+ }
+ }
+
+ private int findPitchPeriodInRange(short[] samples, int position, int minPeriod, int maxPeriod) {
+ // Find the best frequency match in the range, and given a sample skip multiple. For now, just
+ // find the pitch of the first channel.
+ int bestPeriod = 0;
+ int worstPeriod = 255;
+ int minDiff = 1;
+ int maxDiff = 0;
+ position *= numChannels;
+ for (int period = minPeriod; period <= maxPeriod; period++) {
+ int diff = 0;
+ for (int i = 0; i < period; i++) {
+ short sVal = samples[position + i];
+ short pVal = samples[position + period + i];
+ diff += sVal >= pVal ? sVal - pVal : pVal - sVal;
+ }
+ // Note that the highest number of samples we add into diff will be less than 256, since we
+ // skip samples. Thus, diff is a 24 bit number, and we can safely multiply by numSamples
+ // without overflow.
+ if (diff * bestPeriod < minDiff * period) {
+ minDiff = diff;
+ bestPeriod = period;
+ }
+ if (diff * worstPeriod > maxDiff * period) {
+ maxDiff = diff;
+ worstPeriod = period;
+ }
+ }
+ this.minDiff = minDiff / bestPeriod;
+ this.maxDiff = maxDiff / worstPeriod;
+ return bestPeriod;
+ }
+
+ /**
+ * Returns whether the previous pitch period estimate is a better approximation, which can occur
+ * at the abrupt end of voiced words.
+ */
+ private boolean previousPeriodBetter(int minDiff, int maxDiff, boolean preferNewPeriod) {
+ if (minDiff == 0 || prevPeriod == 0) {
+ return false;
+ }
+ if (preferNewPeriod) {
+ if (maxDiff > minDiff * 3) {
+ // Got a reasonable match this period
+ return false;
+ }
+ if (minDiff * 2 <= prevMinDiff * 3) {
+ // Mismatch is not that much greater this period
+ return false;
+ }
+ } else {
+ if (minDiff <= prevMinDiff) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private int findPitchPeriod(short[] samples, int position, boolean preferNewPeriod) {
+ // Find the pitch period. This is a critical step, and we may have to try multiple ways to get a
+ // good answer. This version uses AMDF. To improve speed, we down sample by an integer factor
+ // get in the 11 kHz range, and then do it again with a narrower frequency range without down
+ // sampling.
+ int period;
+ int retPeriod;
+ int skip = sampleRate > AMDF_FREQUENCY ? sampleRate / AMDF_FREQUENCY : 1;
+ if (numChannels == 1 && skip == 1) {
+ period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);
+ } else {
+ downSampleInput(samples, position, skip);
+ period = findPitchPeriodInRange(downSampleBuffer, 0, minPeriod / skip, maxPeriod / skip);
+ if (skip != 1) {
+ period *= skip;
+ int minP = period - (skip * 4);
+ int maxP = period + (skip * 4);
+ if (minP < minPeriod) {
+ minP = minPeriod;
+ }
+ if (maxP > maxPeriod) {
+ maxP = maxPeriod;
+ }
+ if (numChannels == 1) {
+ period = findPitchPeriodInRange(samples, position, minP, maxP);
+ } else {
+ downSampleInput(samples, position, 1);
+ period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP);
+ }
+ }
+ }
+ if (previousPeriodBetter(minDiff, maxDiff, preferNewPeriod)) {
+ retPeriod = prevPeriod;
+ } else {
+ retPeriod = period;
+ }
+ prevMinDiff = minDiff;
+ prevPeriod = period;
+ return retPeriod;
+ }
+
+ private void moveNewSamplesToPitchBuffer(int originalNumOutputSamples) {
+ int numSamples = numOutputSamples - originalNumOutputSamples;
+ if (numPitchSamples + numSamples > pitchBufferSize) {
+ pitchBufferSize += (pitchBufferSize / 2) + numSamples;
+ pitchBuffer = Arrays.copyOf(pitchBuffer, pitchBufferSize * numChannels);
+ }
+ System.arraycopy(outputBuffer, originalNumOutputSamples * numChannels, pitchBuffer,
+ numPitchSamples * numChannels, numSamples * numChannels);
+ numOutputSamples = originalNumOutputSamples;
+ numPitchSamples += numSamples;
+ }
+
+ private void removePitchSamples(int numSamples) {
+ if (numSamples == 0) {
+ return;
+ }
+ System.arraycopy(pitchBuffer, numSamples * numChannels, pitchBuffer, 0,
+ (numPitchSamples - numSamples) * numChannels);
+ numPitchSamples -= numSamples;
+ }
+
+ private void adjustPitch(int originalNumOutputSamples) {
+ // Latency due to pitch changes could be reduced by looking at past samples to determine pitch,
+ // rather than future.
+ if (numOutputSamples == originalNumOutputSamples) {
+ return;
+ }
+ moveNewSamplesToPitchBuffer(originalNumOutputSamples);
+ int position = 0;
+ while (numPitchSamples - position >= maxRequired) {
+ int period = findPitchPeriod(pitchBuffer, position, false);
+ int newPeriod = (int) (period / pitch);
+ enlargeOutputBufferIfNeeded(newPeriod);
+ if (pitch >= 1.0f) {
+ overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer, position,
+ pitchBuffer, position + period - newPeriod);
+ } else {
+ int separation = newPeriod - period;
+ overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples,
+ pitchBuffer, position, pitchBuffer, position);
+ }
+ numOutputSamples += newPeriod;
+ position += period;
+ }
+ removePitchSamples(position);
+ }
+
+ private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) {
+ short left = in[inPos * numChannels];
+ short right = in[inPos * numChannels + numChannels];
+ int position = newRatePosition * oldSampleRate;
+ int leftPosition = oldRatePosition * newSampleRate;
+ int rightPosition = (oldRatePosition + 1) * newSampleRate;
+ int ratio = rightPosition - position;
+ int width = rightPosition - leftPosition;
+ return (short) ((ratio * left + (width - ratio) * right) / width);
+ }
+
+ private void adjustRate(float rate, int originalNumOutputSamples) {
+ if (numOutputSamples == originalNumOutputSamples) {
+ return;
+ }
+ int newSampleRate = (int) (sampleRate / rate);
+ int oldSampleRate = sampleRate;
+ // Set these values to help with the integer math.
+ while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) {
+ newSampleRate /= 2;
+ oldSampleRate /= 2;
+ }
+ moveNewSamplesToPitchBuffer(originalNumOutputSamples);
+ // Leave at least one pitch sample in the buffer.
+ for (int position = 0; position < numPitchSamples - 1; position++) {
+ while ((oldRatePosition + 1) * newSampleRate > newRatePosition * oldSampleRate) {
+ enlargeOutputBufferIfNeeded(1);
+ for (int i = 0; i < numChannels; i++) {
+ outputBuffer[numOutputSamples * numChannels + i] =
+ interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate);
+ }
+ newRatePosition++;
+ numOutputSamples++;
+ }
+ oldRatePosition++;
+ if (oldRatePosition == oldSampleRate) {
+ oldRatePosition = 0;
+ Assertions.checkState(newRatePosition == newSampleRate);
+ newRatePosition = 0;
+ }
+ }
+ removePitchSamples(numPitchSamples - 1);
+ }
+
+ private int skipPitchPeriod(short[] samples, int position, float speed, int period) {
+ // Skip over a pitch period, and copy period/speed samples to the output.
+ int newSamples;
+ if (speed >= 2.0f) {
+ newSamples = (int) (period / (speed - 1.0f));
+ } else {
+ newSamples = period;
+ remainingInputToCopy = (int) (period * (2.0f - speed) / (speed - 1.0f));
+ }
+ enlargeOutputBufferIfNeeded(newSamples);
+ overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position, samples,
+ position + period);
+ numOutputSamples += newSamples;
+ return newSamples;
+ }
+
+ private int insertPitchPeriod(short[] samples, int position, float speed, int period) {
+ // Insert a pitch period, and determine how much input to copy directly.
+ int newSamples;
+ if (speed < 0.5f) {
+ newSamples = (int) (period * speed / (1.0f - speed));
+ } else {
+ newSamples = period;
+ remainingInputToCopy = (int) (period * (2.0f * speed - 1.0f) / (1.0f - speed));
+ }
+ enlargeOutputBufferIfNeeded(period + newSamples);
+ System.arraycopy(samples, position * numChannels, outputBuffer, numOutputSamples * numChannels,
+ period * numChannels);
+ overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples,
+ position + period, samples, position);
+ numOutputSamples += period + newSamples;
+ return newSamples;
+ }
+
+ private void changeSpeed(float speed) {
+ if (numInputSamples < maxRequired) {
+ return;
+ }
+ int numSamples = numInputSamples;
+ int position = 0;
+ do {
+ if (remainingInputToCopy > 0) {
+ position += copyInputToOutput(position);
+ } else {
+ int period = findPitchPeriod(inputBuffer, position, true);
+ if (speed > 1.0) {
+ position += period + skipPitchPeriod(inputBuffer, position, speed, period);
+ } else {
+ position += insertPitchPeriod(inputBuffer, position, speed, period);
+ }
+ }
+ } while (position + maxRequired <= numSamples);
+ removeProcessedInputSamples(position);
+ }
+
+ private void processStreamInput() {
+ // Resample as many pitch periods as we have buffered on the input.
+ int originalNumOutputSamples = numOutputSamples;
+ float s = speed / pitch;
+ if (s > 1.00001 || s < 0.99999) {
+ changeSpeed(s);
+ } else {
+ copyToOutput(inputBuffer, 0, numInputSamples);
+ numInputSamples = 0;
+ }
+ if (USE_CHORD_PITCH) {
+ if (pitch != 1.0f) {
+ adjustPitch(originalNumOutputSamples);
+ }
+ } else if (!USE_CHORD_PITCH && pitch != 1.0f) {
+ adjustRate(pitch, originalNumOutputSamples);
+ }
+ }
+
+ private static void overlapAdd(int numSamples, int numChannels, short[] out, int outPos,
+ short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
+ for (int i = 0; i < numChannels; i++) {
+ int o = outPos * numChannels + i;
+ int u = rampUpPos * numChannels + i;
+ int d = rampDownPos * numChannels + i;
+ for (int t = 0; t < numSamples; t++) {
+ out[o] = (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * t) / numSamples);
+ o += numChannels;
+ d += numChannels;
+ u += numChannels;
+ }
+ }
+ }
+
+ private static void overlapAddWithSeparation(int numSamples, int numChannels, int separation,
+ short[] out, int outPos, short[] rampDown, int rampDownPos, short[] rampUp, int rampUpPos) {
+ for (int i = 0; i < numChannels; i++) {
+ int o = outPos * numChannels + i;
+ int u = rampUpPos * numChannels + i;
+ int d = rampDownPos * numChannels + i;
+ for (int t = 0; t < numSamples + separation; t++) {
+ if (t < separation) {
+ out[o] = (short) (rampDown[d] * (numSamples - t) / numSamples);
+ d += numChannels;
+ } else if (t < numSamples) {
+ out[o] =
+ (short) ((rampDown[d] * (numSamples - t) + rampUp[u] * (t - separation))
+ / numSamples);
+ d += numChannels;
+ u += numChannels;
+ } else {
+ out[o] = (short) (rampUp[u] * (t - separation) / numSamples);
+ u += numChannels;
+ }
+ o += numChannels;
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java
@@ -0,0 +1,212 @@
+/*
+ * 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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.C.Encoding;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+
+/**
+ * An {@link AudioProcessor} that uses the Sonic library to modify the speed/pitch of audio.
+ */
+public final class SonicAudioProcessor implements AudioProcessor {
+
+ /**
+ * The maximum allowed playback speed in {@link #setSpeed(float)}.
+ */
+ public static final float MAXIMUM_SPEED = 8.0f;
+ /**
+ * The minimum allowed playback speed in {@link #setSpeed(float)}.
+ */
+ public static final float MINIMUM_SPEED = 0.1f;
+ /**
+ * The maximum allowed pitch in {@link #setPitch(float)}.
+ */
+ public static final float MAXIMUM_PITCH = 8.0f;
+ /**
+ * The minimum allowed pitch in {@link #setPitch(float)}.
+ */
+ public static final float MINIMUM_PITCH = 0.1f;
+
+ /**
+ * The threshold below which the difference between two pitch/speed factors is negligible.
+ */
+ private static final float CLOSE_THRESHOLD = 0.01f;
+
+ private int channelCount;
+ private int sampleRateHz;
+
+ private Sonic sonic;
+ private float speed;
+ private float pitch;
+
+ private ByteBuffer buffer;
+ private ShortBuffer shortBuffer;
+ private ByteBuffer outputBuffer;
+ private long inputBytes;
+ private long outputBytes;
+ private boolean inputEnded;
+
+ /**
+ * Creates a new Sonic audio processor.
+ */
+ public SonicAudioProcessor() {
+ speed = 1f;
+ pitch = 1f;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ buffer = EMPTY_BUFFER;
+ shortBuffer = buffer.asShortBuffer();
+ outputBuffer = EMPTY_BUFFER;
+ }
+
+ /**
+ * Sets the playback speed. The new speed will take effect after a call to {@link #flush()}.
+ *
+ * @param speed The requested new playback speed.
+ * @return The actual new playback speed.
+ */
+ public float setSpeed(float speed) {
+ this.speed = Util.constrainValue(speed, MINIMUM_SPEED, MAXIMUM_SPEED);
+ return this.speed;
+ }
+
+ /**
+ * Sets the playback pitch. The new pitch will take effect after a call to {@link #flush()}.
+ *
+ * @param pitch The requested new pitch.
+ * @return The actual new pitch.
+ */
+ public float setPitch(float pitch) {
+ this.pitch = Util.constrainValue(pitch, MINIMUM_PITCH, MAXIMUM_PITCH);
+ return pitch;
+ }
+
+ /**
+ * Returns the number of bytes of input queued since the last call to {@link #flush()}.
+ */
+ public long getInputByteCount() {
+ return inputBytes;
+ }
+
+ /**
+ * Returns the number of bytes of output dequeued since the last call to {@link #flush()}.
+ */
+ public long getOutputByteCount() {
+ return outputBytes;
+ }
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
+ throws UnhandledFormatException {
+ if (encoding != C.ENCODING_PCM_16BIT) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
+ return false;
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ return true;
+ }
+
+ @Override
+ public boolean isActive() {
+ return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD;
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return channelCount;
+ }
+
+ @Override
+ public int getOutputEncoding() {
+ return C.ENCODING_PCM_16BIT;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ if (inputBuffer.hasRemaining()) {
+ ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
+ int inputSize = inputBuffer.remaining();
+ inputBytes += inputSize;
+ sonic.queueInput(shortBuffer);
+ inputBuffer.position(inputBuffer.position() + inputSize);
+ }
+ int outputSize = sonic.getSamplesAvailable() * channelCount * 2;
+ if (outputSize > 0) {
+ if (buffer.capacity() < outputSize) {
+ buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
+ shortBuffer = buffer.asShortBuffer();
+ } else {
+ buffer.clear();
+ shortBuffer.clear();
+ }
+ sonic.getOutput(shortBuffer);
+ outputBytes += outputSize;
+ buffer.limit(outputSize);
+ outputBuffer = buffer;
+ }
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ sonic.queueEndOfStream();
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return inputEnded && (sonic == null || sonic.getSamplesAvailable() == 0);
+ }
+
+ @Override
+ public void flush() {
+ sonic = new Sonic(sampleRateHz, channelCount);
+ sonic.setSpeed(speed);
+ sonic.setPitch(pitch);
+ outputBuffer = EMPTY_BUFFER;
+ inputBytes = 0;
+ outputBytes = 0;
+ inputEnded = false;
+ }
+
+ @Override
+ public void reset() {
+ sonic = null;
+ buffer = EMPTY_BUFFER;
+ shortBuffer = buffer.asShortBuffer();
+ outputBuffer = EMPTY_BUFFER;
+ channelCount = Format.NO_VALUE;
+ sampleRateHz = Format.NO_VALUE;
+ inputBytes = 0;
+ outputBytes = 0;
+ inputEnded = false;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/Buffer.java
@@ -0,0 +1,95 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * Base class for buffers with flags.
+ */
+public abstract class Buffer {
+
+ @C.BufferFlags
+ private int flags;
+
+ /**
+ * Clears the buffer.
+ */
+ public void clear() {
+ flags = 0;
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_DECODE_ONLY} flag is set.
+ */
+ public final boolean isDecodeOnly() {
+ return getFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set.
+ */
+ public final boolean isEndOfStream() {
+ return getFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_KEY_FRAME} flag is set.
+ */
+ public final boolean isKeyFrame() {
+ return getFlag(C.BUFFER_FLAG_KEY_FRAME);
+ }
+
+ /**
+ * Replaces this buffer's flags with {@code flags}.
+ *
+ * @param flags The flags to set, which should be a combination of the {@code C.BUFFER_FLAG_*}
+ * constants.
+ */
+ public final void setFlags(@C.BufferFlags int flags) {
+ this.flags = flags;
+ }
+
+ /**
+ * Adds the {@code flag} to this buffer's flags.
+ *
+ * @param flag The flag to add to this buffer's flags, which should be one of the
+ * {@code C.BUFFER_FLAG_*} constants.
+ */
+ public final void addFlag(@C.BufferFlags int flag) {
+ flags |= flag;
+ }
+
+ /**
+ * Removes the {@code flag} from this buffer's flags, if it is set.
+ *
+ * @param flag The flag to remove.
+ */
+ public final void clearFlag(@C.BufferFlags int flag) {
+ flags &= ~flag;
+ }
+
+ /**
+ * Returns whether the specified flag has been set on this buffer.
+ *
+ * @param flag The flag to check.
+ * @return Whether the flag is set.
+ */
+ protected final boolean getFlag(@C.BufferFlags int flag) {
+ return (flags & flag) == flag;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+import android.annotation.TargetApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Compatibility wrapper for {@link android.media.MediaCodec.CryptoInfo}.
+ */
+public final class CryptoInfo {
+
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#iv
+ */
+ public byte[] iv;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#key
+ */
+ public byte[] key;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#mode
+ */
+ @C.CryptoMode
+ public int mode;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
+ */
+ public int[] numBytesOfClearData;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
+ */
+ public int[] numBytesOfEncryptedData;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#numSubSamples
+ */
+ public int numSubSamples;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo.Pattern
+ */
+ public int patternBlocksToEncrypt;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo.Pattern
+ */
+ public int patternBlocksToSkip;
+
+ private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
+ private final PatternHolderV24 patternHolder;
+
+ public CryptoInfo() {
+ frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
+ patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;
+ }
+
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
+ */
+ public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
+ byte[] key, byte[] iv, @C.CryptoMode int mode) {
+ this.numSubSamples = numSubSamples;
+ this.numBytesOfClearData = numBytesOfClearData;
+ this.numBytesOfEncryptedData = numBytesOfEncryptedData;
+ this.key = key;
+ this.iv = iv;
+ this.mode = mode;
+ patternBlocksToEncrypt = 0;
+ patternBlocksToSkip = 0;
+ if (Util.SDK_INT >= 16) {
+ updateFrameworkCryptoInfoV16();
+ }
+ }
+
+ public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) {
+ this.patternBlocksToEncrypt = patternBlocksToEncrypt;
+ this.patternBlocksToSkip = patternBlocksToSkip;
+ if (Util.SDK_INT >= 24) {
+ patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip);
+ }
+ }
+
+ /**
+ * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+ * <p>
+ * Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
+ * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
+ * should not be modified directly.
+ *
+ * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+ */
+ @TargetApi(16)
+ public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
+ return frameworkCryptoInfo;
+ }
+
+ @TargetApi(16)
+ private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
+ return new android.media.MediaCodec.CryptoInfo();
+ }
+
+ @TargetApi(16)
+ private void updateFrameworkCryptoInfoV16() {
+ // Update fields directly because the framework's CryptoInfo.set performs an unnecessary object
+ // allocation on Android N.
+ frameworkCryptoInfo.numSubSamples = numSubSamples;
+ frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;
+ frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData;
+ frameworkCryptoInfo.key = key;
+ frameworkCryptoInfo.iv = iv;
+ frameworkCryptoInfo.mode = mode;
+ if (Util.SDK_INT >= 24) {
+ patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip);
+ }
+ }
+
+ @TargetApi(24)
+ private static final class PatternHolderV24 {
+
+ private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
+ private final android.media.MediaCodec.CryptoInfo.Pattern pattern;
+
+ private PatternHolderV24(android.media.MediaCodec.CryptoInfo frameworkCryptoInfo) {
+ this.frameworkCryptoInfo = frameworkCryptoInfo;
+ pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0);
+ }
+
+ private void set(int blocksToEncrypt, int blocksToSkip) {
+ pattern.set(blocksToEncrypt, blocksToSkip);
+ frameworkCryptoInfo.setPattern(pattern);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/Decoder.java
@@ -0,0 +1,69 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+/**
+ * A media decoder.
+ *
+ * @param <I> The type of buffer input to the decoder.
+ * @param <O> The type of buffer output from the decoder.
+ * @param <E> The type of exception thrown from the decoder.
+ */
+public interface Decoder<I, O, E extends Exception> {
+
+ /**
+ * Returns the name of the decoder.
+ *
+ * @return The name of the decoder.
+ */
+ String getName();
+
+ /**
+ * Dequeues the next input buffer to be filled and queued to the decoder.
+ *
+ * @return The input buffer, which will have been cleared, or null if a buffer isn't available.
+ * @throws E If a decoder error has occurred.
+ */
+ I dequeueInputBuffer() throws E;
+
+ /**
+ * Queues an input buffer to the decoder.
+ *
+ * @param inputBuffer The input buffer.
+ * @throws E If a decoder error has occurred.
+ */
+ void queueInputBuffer(I inputBuffer) throws E;
+
+ /**
+ * Dequeues the next output buffer from the decoder.
+ *
+ * @return The output buffer, or null if an output buffer isn't available.
+ * @throws E If a decoder error has occurred.
+ */
+ O dequeueOutputBuffer() throws E;
+
+ /**
+ * Flushes the decoder. Ownership of dequeued input buffers is returned to the decoder. The caller
+ * is still responsible for releasing any dequeued output buffers.
+ */
+ void flush();
+
+ /**
+ * Releases the decoder. Must be called when the decoder is no longer needed.
+ */
+ void release();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/DecoderCounters.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+/**
+ * Maintains decoder event counts, for debugging purposes only.
+ * <p>
+ * Counters should be written from the playback thread only. Counters may be read from any thread.
+ * To ensure that the counter values are made visible across threads, users of this class should
+ * invoke {@link #ensureUpdated()} prior to reading and after writing.
+ */
+public final class DecoderCounters {
+
+ /**
+ * The number of times a decoder has been initialized.
+ */
+ public int decoderInitCount;
+ /**
+ * The number of times a decoder has been released.
+ */
+ public int decoderReleaseCount;
+ /**
+ * The number of queued input buffers.
+ */
+ public int inputBufferCount;
+ /**
+ * The number of rendered output buffers.
+ */
+ public int renderedOutputBufferCount;
+ /**
+ * The number of skipped output buffers.
+ * <p>
+ * A skipped output buffer is an output buffer that was deliberately not rendered.
+ */
+ public int skippedOutputBufferCount;
+ /**
+ * The number of dropped output buffers.
+ * <p>
+ * A dropped output buffer is an output buffer that was supposed to be rendered, but was instead
+ * dropped because it could not be rendered in time.
+ */
+ public int droppedOutputBufferCount;
+ /**
+ * The maximum number of dropped output buffers without an interleaving rendered output buffer.
+ * <p>
+ * Skipped output buffers are ignored for the purposes of calculating this value.
+ */
+ public int maxConsecutiveDroppedOutputBufferCount;
+
+ /**
+ * Should be called to ensure counter values are made visible across threads. The playback thread
+ * should call this method after updating the counter values. Any other thread should call this
+ * method before reading the counters.
+ */
+ public synchronized void ensureUpdated() {
+ // Do nothing. The use of synchronized ensures a memory barrier should another thread also
+ // call this method.
+ }
+
+ /**
+ * Merges the counts from {@code other} into this instance.
+ *
+ * @param other The {@link DecoderCounters} to merge into this instance.
+ */
+ public void merge(DecoderCounters other) {
+ decoderInitCount += other.decoderInitCount;
+ decoderReleaseCount += other.decoderReleaseCount;
+ inputBufferCount += other.inputBufferCount;
+ renderedOutputBufferCount += other.renderedOutputBufferCount;
+ skippedOutputBufferCount += other.skippedOutputBufferCount;
+ droppedOutputBufferCount += other.droppedOutputBufferCount;
+ maxConsecutiveDroppedOutputBufferCount = Math.max(maxConsecutiveDroppedOutputBufferCount,
+ other.maxConsecutiveDroppedOutputBufferCount);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
@@ -0,0 +1,165 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+
+/**
+ * Holds input for a decoder.
+ */
+public class DecoderInputBuffer extends Buffer {
+
+ /**
+ * The buffer replacement mode, which may disable replacement.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({BUFFER_REPLACEMENT_MODE_DISABLED, BUFFER_REPLACEMENT_MODE_NORMAL,
+ BUFFER_REPLACEMENT_MODE_DIRECT})
+ public @interface BufferReplacementMode {}
+ /**
+ * Disallows buffer replacement.
+ */
+ public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;
+ /**
+ * Allows buffer replacement using {@link ByteBuffer#allocate(int)}.
+ */
+ public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;
+ /**
+ * Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.
+ */
+ public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2;
+
+ /**
+ * {@link CryptoInfo} for encrypted data.
+ */
+ public final CryptoInfo cryptoInfo;
+
+ /**
+ * The buffer's data, or {@code null} if no data has been set.
+ */
+ public ByteBuffer data;
+
+ /**
+ * The time at which the sample should be presented.
+ */
+ public long timeUs;
+
+ @BufferReplacementMode private final int bufferReplacementMode;
+
+ /**
+ * Creates a new instance for which {@link #isFlagsOnly()} will return true.
+ *
+ * @return A new flags only input buffer.
+ */
+ public static DecoderInputBuffer newFlagsOnlyInstance() {
+ return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
+ }
+
+ /**
+ * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One
+ * of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and
+ * {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.
+ */
+ public DecoderInputBuffer(@BufferReplacementMode int bufferReplacementMode) {
+ this.cryptoInfo = new CryptoInfo();
+ this.bufferReplacementMode = bufferReplacementMode;
+ }
+
+ /**
+ * Ensures that {@link #data} is large enough to accommodate a write of a given length at its
+ * current position.
+ * <p>
+ * If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is
+ * insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer}
+ * whose capacity is sufficient. Data up to the current position is copied to the new buffer.
+ *
+ * @param length The length of the write that must be accommodated, in bytes.
+ * @throws IllegalStateException If there is insufficient capacity to accommodate the write and
+ * the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.
+ */
+ public void ensureSpaceForWrite(int length) throws IllegalStateException {
+ if (data == null) {
+ data = createReplacementByteBuffer(length);
+ return;
+ }
+ // Check whether the current buffer is sufficient.
+ int capacity = data.capacity();
+ int position = data.position();
+ int requiredCapacity = position + length;
+ if (capacity >= requiredCapacity) {
+ return;
+ }
+ // Instantiate a new buffer if possible.
+ ByteBuffer newData = createReplacementByteBuffer(requiredCapacity);
+ // Copy data up to the current position from the old buffer to the new one.
+ if (position > 0) {
+ data.position(0);
+ data.limit(position);
+ newData.put(data);
+ }
+ // Set the new buffer.
+ data = newData;
+ }
+
+ /**
+ * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and
+ * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.
+ */
+ public final boolean isFlagsOnly() {
+ return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED;
+ }
+
+ /**
+ * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set.
+ */
+ public final boolean isEncrypted() {
+ return getFlag(C.BUFFER_FLAG_ENCRYPTED);
+ }
+
+ /**
+ * Flips {@link #data} in preparation for being queued to a decoder.
+ *
+ * @see java.nio.Buffer#flip()
+ */
+ public final void flip() {
+ data.flip();
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ if (data != null) {
+ data.clear();
+ }
+ }
+
+ private ByteBuffer createReplacementByteBuffer(int requiredCapacity) {
+ if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_NORMAL) {
+ return ByteBuffer.allocate(requiredCapacity);
+ } else if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DIRECT) {
+ return ByteBuffer.allocateDirect(requiredCapacity);
+ } else {
+ int currentCapacity = data == null ? 0 : data.capacity();
+ throw new IllegalStateException("Buffer too small (" + currentCapacity + " < "
+ + requiredCapacity + ")");
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/OutputBuffer.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+/**
+ * Output buffer decoded by a {@link Decoder}.
+ */
+public abstract class OutputBuffer extends Buffer {
+
+ /**
+ * The presentation timestamp for the buffer, in microseconds.
+ */
+ public long timeUs;
+
+ /**
+ * The number of buffers immediately prior to this one that were skipped in the {@link Decoder}.
+ */
+ public int skippedOutputBufferCount;
+
+ /**
+ * Releases the output buffer for reuse. Must be called when the buffer is no longer needed.
+ */
+ public abstract void release();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
@@ -0,0 +1,286 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.LinkedList;
+
+/**
+ * Base class for {@link Decoder}s that use their own decode thread.
+ */
+public abstract class SimpleDecoder<I extends DecoderInputBuffer, O extends OutputBuffer,
+ E extends Exception> implements Decoder<I, O, E> {
+
+ private final Thread decodeThread;
+
+ private final Object lock;
+ private final LinkedList<I> queuedInputBuffers;
+ private final LinkedList<O> queuedOutputBuffers;
+ private final I[] availableInputBuffers;
+ private final O[] availableOutputBuffers;
+
+ private int availableInputBufferCount;
+ private int availableOutputBufferCount;
+ private I dequeuedInputBuffer;
+
+ private E exception;
+ private boolean flushed;
+ private boolean released;
+ private int skippedOutputBufferCount;
+
+ /**
+ * @param inputBuffers An array of nulls that will be used to store references to input buffers.
+ * @param outputBuffers An array of nulls that will be used to store references to output buffers.
+ */
+ protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {
+ lock = new Object();
+ queuedInputBuffers = new LinkedList<>();
+ queuedOutputBuffers = new LinkedList<>();
+ availableInputBuffers = inputBuffers;
+ availableInputBufferCount = inputBuffers.length;
+ for (int i = 0; i < availableInputBufferCount; i++) {
+ availableInputBuffers[i] = createInputBuffer();
+ }
+ availableOutputBuffers = outputBuffers;
+ availableOutputBufferCount = outputBuffers.length;
+ for (int i = 0; i < availableOutputBufferCount; i++) {
+ availableOutputBuffers[i] = createOutputBuffer();
+ }
+ decodeThread = new Thread() {
+ @Override
+ public void run() {
+ SimpleDecoder.this.run();
+ }
+ };
+ decodeThread.start();
+ }
+
+ /**
+ * Sets the initial size of each input buffer.
+ * <p>
+ * This method should only be called before the decoder is used (i.e. before the first call to
+ * {@link #dequeueInputBuffer()}.
+ *
+ * @param size The required input buffer size.
+ */
+ protected final void setInitialInputBufferSize(int size) {
+ Assertions.checkState(availableInputBufferCount == availableInputBuffers.length);
+ for (I inputBuffer : availableInputBuffers) {
+ inputBuffer.ensureSpaceForWrite(size);
+ }
+ }
+
+ @Override
+ public final I dequeueInputBuffer() throws E {
+ synchronized (lock) {
+ maybeThrowException();
+ Assertions.checkState(dequeuedInputBuffer == null);
+ dequeuedInputBuffer = availableInputBufferCount == 0 ? null
+ : availableInputBuffers[--availableInputBufferCount];
+ return dequeuedInputBuffer;
+ }
+ }
+
+ @Override
+ public final void queueInputBuffer(I inputBuffer) throws E {
+ synchronized (lock) {
+ maybeThrowException();
+ Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
+ queuedInputBuffers.addLast(inputBuffer);
+ maybeNotifyDecodeLoop();
+ dequeuedInputBuffer = null;
+ }
+ }
+
+ @Override
+ public final O dequeueOutputBuffer() throws E {
+ synchronized (lock) {
+ maybeThrowException();
+ if (queuedOutputBuffers.isEmpty()) {
+ return null;
+ }
+ return queuedOutputBuffers.removeFirst();
+ }
+ }
+
+ /**
+ * Releases an output buffer back to the decoder.
+ *
+ * @param outputBuffer The output buffer being released.
+ */
+ protected void releaseOutputBuffer(O outputBuffer) {
+ synchronized (lock) {
+ releaseOutputBufferInternal(outputBuffer);
+ maybeNotifyDecodeLoop();
+ }
+ }
+
+ @Override
+ public final void flush() {
+ synchronized (lock) {
+ flushed = true;
+ skippedOutputBufferCount = 0;
+ if (dequeuedInputBuffer != null) {
+ releaseInputBufferInternal(dequeuedInputBuffer);
+ dequeuedInputBuffer = null;
+ }
+ while (!queuedInputBuffers.isEmpty()) {
+ releaseInputBufferInternal(queuedInputBuffers.removeFirst());
+ }
+ while (!queuedOutputBuffers.isEmpty()) {
+ releaseOutputBufferInternal(queuedOutputBuffers.removeFirst());
+ }
+ }
+ }
+
+ @Override
+ public void release() {
+ synchronized (lock) {
+ released = true;
+ lock.notify();
+ }
+ try {
+ decodeThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Throws a decode exception, if there is one.
+ *
+ * @throws E The decode exception.
+ */
+ private void maybeThrowException() throws E {
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ /**
+ * Notifies the decode loop if there exists a queued input buffer and an available output buffer
+ * to decode into.
+ * <p>
+ * Should only be called whilst synchronized on the lock object.
+ */
+ private void maybeNotifyDecodeLoop() {
+ if (canDecodeBuffer()) {
+ lock.notify();
+ }
+ }
+
+ private void run() {
+ try {
+ while (decode()) {
+ // Do nothing.
+ }
+ } catch (InterruptedException e) {
+ // Not expected.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private boolean decode() throws InterruptedException {
+ I inputBuffer;
+ O outputBuffer;
+ boolean resetDecoder;
+
+ // Wait until we have an input buffer to decode, and an output buffer to decode into.
+ synchronized (lock) {
+ while (!released && !canDecodeBuffer()) {
+ lock.wait();
+ }
+ if (released) {
+ return false;
+ }
+ inputBuffer = queuedInputBuffers.removeFirst();
+ outputBuffer = availableOutputBuffers[--availableOutputBufferCount];
+ resetDecoder = flushed;
+ flushed = false;
+ }
+
+ if (inputBuffer.isEndOfStream()) {
+ outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ } else {
+ if (inputBuffer.isDecodeOnly()) {
+ outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ exception = decode(inputBuffer, outputBuffer, resetDecoder);
+ if (exception != null) {
+ // Memory barrier to ensure that the decoder exception is visible from the playback thread.
+ synchronized (lock) {}
+ return false;
+ }
+ }
+
+ synchronized (lock) {
+ if (flushed) {
+ releaseOutputBufferInternal(outputBuffer);
+ } else if (outputBuffer.isDecodeOnly()) {
+ skippedOutputBufferCount++;
+ releaseOutputBufferInternal(outputBuffer);
+ } else {
+ outputBuffer.skippedOutputBufferCount = skippedOutputBufferCount;
+ skippedOutputBufferCount = 0;
+ queuedOutputBuffers.addLast(outputBuffer);
+ }
+ // Make the input buffer available again.
+ releaseInputBufferInternal(inputBuffer);
+ }
+
+ return true;
+ }
+
+ private boolean canDecodeBuffer() {
+ return !queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0;
+ }
+
+ private void releaseInputBufferInternal(I inputBuffer) {
+ inputBuffer.clear();
+ availableInputBuffers[availableInputBufferCount++] = inputBuffer;
+ }
+
+ private void releaseOutputBufferInternal(O outputBuffer) {
+ outputBuffer.clear();
+ availableOutputBuffers[availableOutputBufferCount++] = outputBuffer;
+ }
+
+ /**
+ * Creates a new input buffer.
+ */
+ protected abstract I createInputBuffer();
+
+ /**
+ * Creates a new output buffer.
+ */
+ protected abstract O createOutputBuffer();
+
+ /**
+ * Decodes the {@code inputBuffer} and stores any decoded output in {@code outputBuffer}.
+ *
+ * @param inputBuffer The buffer to decode.
+ * @param outputBuffer The output buffer to store decoded data. The flag
+ * {@link C#BUFFER_FLAG_DECODE_ONLY} will be set if the same flag is set on
+ * {@code inputBuffer}, but may be set/unset as required. If the flag is set when the call
+ * returns then the output buffer will not be made available to dequeue. The output buffer
+ * may not have been populated in this case.
+ * @param reset Whether the decoder must be reset before decoding.
+ * @return A decoder exception if an error occurred, or null if decoding was successful.
+ */
+ protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.google.android.exoplayer2.decoder;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Buffer for {@link SimpleDecoder} output.
+ */
+public class SimpleOutputBuffer extends OutputBuffer {
+
+ private final SimpleDecoder<?, SimpleOutputBuffer, ?> owner;
+
+ public ByteBuffer data;
+
+ public SimpleOutputBuffer(SimpleDecoder<?, SimpleOutputBuffer, ?> owner) {
+ this.owner = owner;
+ }
+
+ /**
+ * Initializes the buffer.
+ *
+ * @param timeUs The presentation timestamp for the buffer, in microseconds.
+ * @param size An upper bound on the size of the data that will be written to the buffer.
+ * @return The {@link #data} buffer, for convenience.
+ */
+ public ByteBuffer init(long timeUs, int size) {
+ this.timeUs = timeUs;
+ if (data == null || data.capacity() < size) {
+ data = ByteBuffer.allocateDirect(size);
+ }
+ data.position(0);
+ data.limit(size);
+ return data;
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ if (data != null) {
+ data.clear();
+ }
+ }
+
+ @Override
+ public void release() {
+ owner.releaseOutputBuffer(this);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/DecryptionException.java
@@ -0,0 +1,20 @@
+package com.google.android.exoplayer2.drm;
+
+/**
+ * An exception when doing drm decryption using the In-App Drm
+ */
+public class DecryptionException extends Exception {
+ private final int errorCode;
+
+ public DecryptionException(int errorCode, String message) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ /**
+ * Get error code
+ */
+ public int getErrorCode() {
+ return errorCode;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -0,0 +1,715 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
+ */
+@TargetApi(18)
+public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
+ DrmSession<T> {
+
+ /**
+ * Listener of {@link DefaultDrmSessionManager} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Called each time keys are loaded.
+ */
+ void onDrmKeysLoaded();
+
+ /**
+ * Called when a drm error occurs.
+ *
+ * @param e The corresponding exception.
+ */
+ void onDrmSessionManagerError(Exception e);
+
+ /**
+ * Called each time offline keys are restored.
+ */
+ void onDrmKeysRestored();
+
+ /**
+ * Called each time offline keys are removed.
+ */
+ void onDrmKeysRemoved();
+
+ }
+
+ /**
+ * The key to use when passing CustomData to a PlayReady instance in an optional parameter map.
+ */
+ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
+
+ /** Determines the action to be done after a session acquired. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})
+ public @interface Mode {}
+ /**
+ * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
+ * licenses.
+ */
+ public static final int MODE_PLAYBACK = 0;
+ /**
+ * Restores an offline license to allow its status to be queried. If the offline license is
+ * expired sets state to {@link #STATE_ERROR}.
+ */
+ public static final int MODE_QUERY = 1;
+ /** Downloads an offline license or renews an existing one. */
+ public static final int MODE_DOWNLOAD = 2;
+ /** Releases an existing offline license. */
+ public static final int MODE_RELEASE = 3;
+
+ private static final String TAG = "OfflineDrmSessionMngr";
+ private static final String CENC_SCHEME_MIME_TYPE = "cenc";
+
+ private static final int MSG_PROVISION = 0;
+ private static final int MSG_KEYS = 1;
+
+ private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
+
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final ExoMediaDrm<T> mediaDrm;
+ private final HashMap<String, String> optionalKeyRequestParameters;
+
+ /* package */ final MediaDrmCallback callback;
+ /* package */ final UUID uuid;
+
+ /* package */ MediaDrmHandler mediaDrmHandler;
+ /* package */ PostResponseHandler postResponseHandler;
+
+ private Looper playbackLooper;
+ private HandlerThread requestHandlerThread;
+ private Handler postRequestHandler;
+
+ private int mode;
+ private int openCount;
+ private boolean provisioningInProgress;
+ @DrmSession.State
+ private int state;
+ private T mediaCrypto;
+ private DrmSessionException lastException;
+ private byte[] schemeInitData;
+ private String schemeMimeType;
+ private byte[] sessionId;
+ private byte[] offlineLicenseKeySetId;
+
+ /**
+ * Instantiates a new instance using the Widevine scheme.
+ *
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
+ */
+ public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
+ MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
+ Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
+ return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
+ eventHandler, eventListener);
+ }
+
+ /**
+ * Instantiates a new instance using the PlayReady scheme.
+ * <p>
+ * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV
+ * devices, which do provide support.
+ *
+ * @param callback Performs key and provisioning requests.
+ * @param customData Optional custom data to include in requests generated by the instance.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
+ */
+ public static DefaultDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(
+ MediaDrmCallback callback, String customData, Handler eventHandler,
+ EventListener eventListener) throws UnsupportedDrmException {
+ HashMap<String, String> optionalKeyRequestParameters;
+ if (!TextUtils.isEmpty(customData)) {
+ optionalKeyRequestParameters = new HashMap<>();
+ optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData);
+ } else {
+ optionalKeyRequestParameters = null;
+ }
+ return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters,
+ eventHandler, eventListener);
+ }
+
+ /**
+ * Instantiates a new instance.
+ *
+ * @param uuid The UUID of the drm scheme.
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
+ */
+ public static DefaultDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(
+ UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
+ Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
+ return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
+ optionalKeyRequestParameters, eventHandler, eventListener);
+ }
+
+ /**
+ * @param uuid The UUID of the drm scheme.
+ * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
+ HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
+ EventListener eventListener) {
+ this.uuid = uuid;
+ this.mediaDrm = mediaDrm;
+ this.callback = callback;
+ this.optionalKeyRequestParameters = optionalKeyRequestParameters;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ mediaDrm.setOnEventListener(new MediaDrmEventListener());
+ state = STATE_CLOSED;
+ mode = MODE_PLAYBACK;
+ }
+
+ /**
+ * Provides access to {@link MediaDrm#getPropertyString(String)}.
+ * <p>
+ * This method may be called when the manager is in any state.
+ *
+ * @param key The key to request.
+ * @return The retrieved property.
+ */
+ public final String getPropertyString(String key) {
+ return mediaDrm.getPropertyString(key);
+ }
+
+ /**
+ * Provides access to {@link MediaDrm#setPropertyString(String, String)}.
+ * <p>
+ * This method may be called when the manager is in any state.
+ *
+ * @param key The property to write.
+ * @param value The value to write.
+ */
+ public final void setPropertyString(String key, String value) {
+ mediaDrm.setPropertyString(key, value);
+ }
+
+ /**
+ * Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
+ * <p>
+ * This method may be called when the manager is in any state.
+ *
+ * @param key The key to request.
+ * @return The retrieved property.
+ */
+ public final byte[] getPropertyByteArray(String key) {
+ return mediaDrm.getPropertyByteArray(key);
+ }
+
+ /**
+ * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}.
+ * <p>
+ * This method may be called when the manager is in any state.
+ *
+ * @param key The property to write.
+ * @param value The value to write.
+ */
+ public final void setPropertyByteArray(String key, byte[] value) {
+ mediaDrm.setPropertyByteArray(key, value);
+ }
+
+ /**
+ * Sets the mode, which determines the role of sessions acquired from the instance. This must be
+ * called before {@link #acquireSession(Looper, DrmInitData)} is called.
+ *
+ * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
+ * required.
+ *
+ * <p>{@code mode} must be one of these:
+ * <ul>
+ * <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
+ * requested otherwise the offline license is restored.
+ * <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
+ * is restored.
+ * <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
+ * requested otherwise the offline license is renewed.
+ * <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
+ * is released.
+ * </ul>
+ *
+ * @param mode The mode to be set.
+ * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
+ */
+ public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) {
+ Assertions.checkState(openCount == 0);
+ if (mode == MODE_QUERY || mode == MODE_RELEASE) {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ }
+ this.mode = mode;
+ this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+ }
+
+ // DrmSessionManager implementation.
+
+ @Override
+ public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
+ Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
+ if (++openCount != 1) {
+ return this;
+ }
+
+ if (this.playbackLooper == null) {
+ this.playbackLooper = playbackLooper;
+ mediaDrmHandler = new MediaDrmHandler(playbackLooper);
+ postResponseHandler = new PostResponseHandler(playbackLooper);
+ }
+
+ requestHandlerThread = new HandlerThread("DrmRequestHandler");
+ requestHandlerThread.start();
+ postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
+
+ if (offlineLicenseKeySetId == null) {
+ SchemeData schemeData = drmInitData.get(uuid);
+ if (schemeData == null) {
+ onError(new IllegalStateException("Media does not support uuid: " + uuid));
+ return this;
+ }
+ schemeInitData = schemeData.data;
+ schemeMimeType = schemeData.mimeType;
+ if (Util.SDK_INT < 21) {
+ // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
+ byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
+ if (psshData == null) {
+ // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
+ } else {
+ schemeInitData = psshData;
+ }
+ }
+ if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
+ && (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
+ || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
+ // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
+ schemeMimeType = CENC_SCHEME_MIME_TYPE;
+ }
+ }
+ state = STATE_OPENING;
+ openInternal(true);
+ return this;
+ }
+
+ @Override
+ public void releaseSession(DrmSession<T> session) {
+ if (--openCount != 0) {
+ return;
+ }
+ state = STATE_CLOSED;
+ provisioningInProgress = false;
+ mediaDrmHandler.removeCallbacksAndMessages(null);
+ postResponseHandler.removeCallbacksAndMessages(null);
+ postRequestHandler.removeCallbacksAndMessages(null);
+ postRequestHandler = null;
+ requestHandlerThread.quit();
+ requestHandlerThread = null;
+ schemeInitData = null;
+ schemeMimeType = null;
+ mediaCrypto = null;
+ lastException = null;
+ if (sessionId != null) {
+ mediaDrm.closeSession(sessionId);
+ sessionId = null;
+ }
+ }
+
+ // DrmSession implementation.
+
+ @Override
+ @DrmSession.State
+ public final int getState() {
+ return state;
+ }
+
+ @Override
+ public final T getMediaCrypto() {
+ if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ throw new IllegalStateException();
+ }
+ return mediaCrypto;
+ }
+
+ @Override
+ public boolean requiresSecureDecoderComponent(String mimeType) {
+ if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ throw new IllegalStateException();
+ }
+ return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+ }
+
+ @Override
+ public final DrmSessionException getError() {
+ return state == STATE_ERROR ? lastException : null;
+ }
+
+ @Override
+ public Map<String, String> queryKeyStatus() {
+ // User may call this method rightfully even if state == STATE_ERROR. So only check if there is
+ // a sessionId
+ if (sessionId == null) {
+ throw new IllegalStateException();
+ }
+ return mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ public byte[] getOfflineLicenseKeySetId() {
+ return offlineLicenseKeySetId;
+ }
+
+ // Internal methods.
+
+ private void openInternal(boolean allowProvisioning) {
+ try {
+ sessionId = mediaDrm.openSession();
+ mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
+ state = STATE_OPENED;
+ doLicense();
+ } catch (NotProvisionedException e) {
+ if (allowProvisioning) {
+ postProvisionRequest();
+ } else {
+ onError(e);
+ }
+ } catch (Exception e) {
+ onError(e);
+ }
+ }
+
+ private void postProvisionRequest() {
+ if (provisioningInProgress) {
+ return;
+ }
+ provisioningInProgress = true;
+ ProvisionRequest request = mediaDrm.getProvisionRequest();
+ postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
+ }
+
+ private void onProvisionResponse(Object response) {
+ provisioningInProgress = false;
+ if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ // This event is stale.
+ return;
+ }
+
+ if (response instanceof Exception) {
+ onError((Exception) response);
+ return;
+ }
+
+ try {
+ mediaDrm.provideProvisionResponse((byte[]) response);
+ if (state == STATE_OPENING) {
+ openInternal(false);
+ } else {
+ doLicense();
+ }
+ } catch (DeniedByServerException e) {
+ onError(e);
+ }
+ }
+
+ private void doLicense() {
+ switch (mode) {
+ case MODE_PLAYBACK:
+ case MODE_QUERY:
+ if (offlineLicenseKeySetId == null) {
+ postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING);
+ } else {
+ if (restoreKeys()) {
+ long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
+ if (mode == MODE_PLAYBACK
+ && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) {
+ Log.d(TAG, "Offline license has expired or will expire soon. "
+ + "Remaining seconds: " + licenseDurationRemainingSec);
+ postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
+ } else if (licenseDurationRemainingSec <= 0) {
+ onError(new KeysExpiredException());
+ } else {
+ state = STATE_OPENED_WITH_KEYS;
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmKeysRestored();
+ }
+ });
+ }
+ }
+ }
+ }
+ break;
+ case MODE_DOWNLOAD:
+ if (offlineLicenseKeySetId == null) {
+ postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
+ } else {
+ // Renew
+ if (restoreKeys()) {
+ postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
+ }
+ }
+ break;
+ case MODE_RELEASE:
+ if (restoreKeys()) {
+ postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
+ }
+ break;
+ }
+ }
+
+ private boolean restoreKeys() {
+ try {
+ mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to restore Widevine keys.", e);
+ onError(e);
+ }
+ return false;
+ }
+
+ private long getLicenseDurationRemainingSec() {
+ if (!C.WIDEVINE_UUID.equals(uuid)) {
+ return Long.MAX_VALUE;
+ }
+ Pair<Long, Long> pair = WidevineUtil.getLicenseDurationRemainingSec(this);
+ return Math.min(pair.first, pair.second);
+ }
+
+ private void postKeyRequest(byte[] scope, int keyType) {
+ try {
+ KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
+ optionalKeyRequestParameters);
+ postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeyResponse(Object response) {
+ if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ // This event is stale.
+ return;
+ }
+
+ if (response instanceof Exception) {
+ onKeysError((Exception) response);
+ return;
+ }
+
+ try {
+ if (mode == MODE_RELEASE) {
+ mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmKeysRemoved();
+ }
+ });
+ }
+ } else {
+ byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
+ if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null))
+ && keySetId != null && keySetId.length != 0) {
+ offlineLicenseKeySetId = keySetId;
+ }
+ state = STATE_OPENED_WITH_KEYS;
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmKeysLoaded();
+ }
+ });
+ }
+ }
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeysError(Exception e) {
+ if (e instanceof NotProvisionedException) {
+ postProvisionRequest();
+ } else {
+ onError(e);
+ }
+ }
+
+ private void onError(final Exception e) {
+ lastException = new DrmSessionException(e);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmSessionManagerError(e);
+ }
+ });
+ }
+ if (state != STATE_OPENED_WITH_KEYS) {
+ state = STATE_ERROR;
+ }
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class MediaDrmHandler extends Handler {
+
+ public MediaDrmHandler(Looper looper) {
+ super(looper);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void handleMessage(Message msg) {
+ if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
+ return;
+ }
+ switch (msg.what) {
+ case MediaDrm.EVENT_KEY_REQUIRED:
+ doLicense();
+ break;
+ case MediaDrm.EVENT_KEY_EXPIRED:
+ // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
+ // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
+ // waiting for key response.
+ if (state == STATE_OPENED_WITH_KEYS) {
+ state = STATE_OPENED;
+ onError(new KeysExpiredException());
+ }
+ break;
+ case MediaDrm.EVENT_PROVISION_REQUIRED:
+ state = STATE_OPENED;
+ postProvisionRequest();
+ break;
+ }
+ }
+
+ }
+
+ private class MediaDrmEventListener implements OnEventListener<T> {
+
+ @Override
+ public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
+ byte[] data) {
+ if (mode == MODE_PLAYBACK) {
+ mediaDrmHandler.sendEmptyMessage(event);
+ }
+ }
+
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class PostResponseHandler extends Handler {
+
+ public PostResponseHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ onProvisionResponse(msg.obj);
+ break;
+ case MSG_KEYS:
+ onKeyResponse(msg.obj);
+ break;
+ }
+ }
+
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class PostRequestHandler extends Handler {
+
+ public PostRequestHandler(Looper backgroundLooper) {
+ super(backgroundLooper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Object response;
+ try {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
+ break;
+ case MSG_KEYS:
+ response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ } catch (Exception e) {
+ response = e;
+ }
+ postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -0,0 +1,282 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Initialization data for one or more DRM schemes.
+ */
+public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
+
+ private final SchemeData[] schemeDatas;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * Number of {@link SchemeData}s.
+ */
+ public final int schemeDataCount;
+
+ /**
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(List<SchemeData> schemeDatas) {
+ this(false, schemeDatas.toArray(new SchemeData[schemeDatas.size()]));
+ }
+
+ /**
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(SchemeData... schemeDatas) {
+ this(true, schemeDatas);
+ }
+
+ private DrmInitData(boolean cloneSchemeDatas, SchemeData... schemeDatas) {
+ if (cloneSchemeDatas) {
+ schemeDatas = schemeDatas.clone();
+ }
+ // Sorting ensures that universal scheme data(i.e. data that applies to all schemes) is matched
+ // last. It's also required by the equals and hashcode implementations.
+ Arrays.sort(schemeDatas, this);
+ // Check for no duplicates.
+ for (int i = 1; i < schemeDatas.length; i++) {
+ if (schemeDatas[i - 1].uuid.equals(schemeDatas[i].uuid)) {
+ throw new IllegalArgumentException("Duplicate data for uuid: " + schemeDatas[i].uuid);
+ }
+ }
+ this.schemeDatas = schemeDatas;
+ schemeDataCount = schemeDatas.length;
+ }
+
+ /* package */ DrmInitData(Parcel in) {
+ schemeDatas = in.createTypedArray(SchemeData.CREATOR);
+ schemeDataCount = schemeDatas.length;
+ }
+
+ /**
+ * Retrieves data for a given DRM scheme, specified by its UUID.
+ *
+ * @param uuid The DRM scheme's UUID.
+ * @return The initialization data for the scheme, or null if the scheme is not supported.
+ */
+ public SchemeData get(UUID uuid) {
+ for (SchemeData schemeData : schemeDatas) {
+ if (schemeData.matches(uuid)) {
+ return schemeData;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the {@link SchemeData} at a given index.
+ *
+ * @param index index of the scheme to return.
+ * @return The {@link SchemeData} at the index.
+ */
+ public SchemeData get(int index) {
+ return schemeDatas[index];
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ hashCode = Arrays.hashCode(schemeDatas);
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ return Arrays.equals(schemeDatas, ((DrmInitData) obj).schemeDatas);
+ }
+
+ @Override
+ public int compare(SchemeData first, SchemeData second) {
+ return C.UUID_NIL.equals(first.uuid) ? (C.UUID_NIL.equals(second.uuid) ? 0 : 1)
+ : first.uuid.compareTo(second.uuid);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedArray(schemeDatas, 0);
+ }
+
+ public static final Parcelable.Creator<DrmInitData> CREATOR =
+ new Parcelable.Creator<DrmInitData>() {
+
+ @Override
+ public DrmInitData createFromParcel(Parcel in) {
+ return new DrmInitData(in);
+ }
+
+ @Override
+ public DrmInitData[] newArray(int size) {
+ return new DrmInitData[size];
+ }
+
+ };
+
+ /**
+ * Scheme initialization data.
+ */
+ public static final class SchemeData implements Parcelable {
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is universal (i.e.
+ * applies to all schemes).
+ */
+ private final UUID uuid;
+ /**
+ * The mimeType of {@link #data}.
+ */
+ public final String mimeType;
+ /**
+ * The initialization data.
+ */
+ public final byte[] data;
+ /**
+ * Whether secure decryption is required.
+ */
+ public final boolean requiresSecureDecryption;
+
+ /**
+ * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
+ * universal (i.e. applies to all schemes).
+ * @param mimeType The mimeType of the initialization data.
+ * @param data The initialization data.
+ */
+ public SchemeData(UUID uuid, String mimeType, byte[] data) {
+ this(uuid, mimeType, data, false);
+ }
+
+ /**
+ * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
+ * universal (i.e. applies to all schemes).
+ * @param mimeType The mimeType of the initialization data.
+ * @param data The initialization data.
+ * @param requiresSecureDecryption Whether secure decryption is required.
+ */
+ public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
+ this.uuid = Assertions.checkNotNull(uuid);
+ this.mimeType = Assertions.checkNotNull(mimeType);
+ this.data = Assertions.checkNotNull(data);
+ this.requiresSecureDecryption = requiresSecureDecryption;
+ }
+
+ /* package */ SchemeData(Parcel in) {
+ uuid = new UUID(in.readLong(), in.readLong());
+ mimeType = in.readString();
+ data = in.createByteArray();
+ requiresSecureDecryption = in.readByte() != 0;
+ }
+
+ /**
+ * Returns whether this initialization data applies to the specified scheme.
+ *
+ * @param schemeUuid The scheme {@link UUID}.
+ * @return Whether this initialization data applies to the specified scheme.
+ */
+ public boolean matches(UUID schemeUuid) {
+ return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SchemeData)) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ SchemeData other = (SchemeData) obj;
+ return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid)
+ && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = uuid.hashCode();
+ result = 31 * result + mimeType.hashCode();
+ result = 31 * result + Arrays.hashCode(data);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(uuid.getMostSignificantBits());
+ dest.writeLong(uuid.getLeastSignificantBits());
+ dest.writeString(mimeType);
+ dest.writeByteArray(data);
+ dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<SchemeData> CREATOR =
+ new Parcelable.Creator<SchemeData>() {
+
+ @Override
+ public SchemeData createFromParcel(Parcel in) {
+ return new SchemeData(in);
+ }
+
+ @Override
+ public SchemeData[] newArray(int size) {
+ return new SchemeData[size];
+ }
+
+ };
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/DrmSession.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaDrm;
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+
+/**
+ * A DRM session.
+ */
+@TargetApi(16)
+public interface DrmSession<T extends ExoMediaCrypto> {
+
+ /** Wraps the exception which is the cause of the error state. */
+ class DrmSessionException extends Exception {
+
+ public DrmSessionException(Exception e) {
+ super(e);
+ }
+
+ }
+
+ /**
+ * The state of the DRM session.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
+ @interface State {}
+ /**
+ * The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
+ */
+ int STATE_ERROR = 0;
+ /**
+ * The session is closed.
+ */
+ int STATE_CLOSED = 1;
+ /**
+ * The session is being opened.
+ */
+ int STATE_OPENING = 2;
+ /**
+ * The session is open, but does not yet have the keys required for decryption.
+ */
+ int STATE_OPENED = 3;
+ /**
+ * The session is open and has the keys required for decryption.
+ */
+ int STATE_OPENED_WITH_KEYS = 4;
+
+ /**
+ * Returns the current state of the session.
+ *
+ * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
+ * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
+ */
+ @State int getState();
+
+ /**
+ * Returns a {@link ExoMediaCrypto} for the open session.
+ * <p>
+ * This method may be called when the session is in the following states:
+ * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+ *
+ * @return A {@link ExoMediaCrypto} for the open session.
+ * @throws IllegalStateException If called when a session isn't opened.
+ */
+ T getMediaCrypto();
+
+ /**
+ * Whether the session requires a secure decoder for the specified mime type.
+ * <p>
+ * Normally this method should return
+ * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases
+ * implementations may wish to modify the return value (i.e. to force a secure decoder even when
+ * one is not required).
+ * <p>
+ * This method may be called when the session is in the following states:
+ * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+ *
+ * @return Whether the open session requires a secure decoder for the specified mime type.
+ * @throws IllegalStateException If called when a session isn't opened.
+ */
+ boolean requiresSecureDecoderComponent(String mimeType);
+
+ /**
+ * Returns the cause of the error state.
+ * <p>
+ * This method may be called when the session is in any state.
+ *
+ * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
+ */
+ DrmSessionException getError();
+
+ /**
+ * Returns an informative description of the key status for the session. The status is in the form
+ * of {name, value} pairs.
+ *
+ * <p>Since DRM license policies vary by vendor, the specific status field names are determined by
+ * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
+ * for a particular DRM engine plugin.
+ *
+ * @return A map of key status.
+ * @throws IllegalStateException If called when the session isn't opened.
+ * @see MediaDrm#queryKeyStatus(byte[])
+ */
+ Map<String, String> queryKeyStatus();
+
+ /**
+ * Returns the key set id of the offline license loaded into this session, if there is one. Null
+ * otherwise.
+ */
+ byte[] getOfflineLicenseKeySetId();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.os.Looper;
+
+/**
+ * Manages a DRM session.
+ */
+@TargetApi(16)
+public interface DrmSessionManager<T extends ExoMediaCrypto> {
+
+ /**
+ * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession}
+ * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required.
+ *
+ * @param playbackLooper The looper associated with the media playback thread.
+ * @param drmInitData DRM initialization data.
+ * @return The DRM session.
+ */
+ DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData);
+
+ /**
+ * Releases a {@link DrmSession}.
+ */
+ void releaseSession(DrmSession<T> drmSession);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+/**
+ * An opaque {@link android.media.MediaCrypto} equivalent.
+ */
+public interface ExoMediaCrypto {
+
+ /**
+ * @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
+ */
+ boolean requiresSecureDecoderComponent(String mimeType);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.media.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}.
+ */
+public interface ExoMediaDrm<T extends ExoMediaCrypto> {
+
+ /**
+ * @see android.media.MediaDrm.OnEventListener
+ */
+ interface OnEventListener<T extends ExoMediaCrypto> {
+ /**
+ * Called when an event occurs that requires the app to be notified
+ *
+ * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred.
+ * @param sessionId the DRM session ID on which the event occurred
+ * @param event indicates the event type
+ * @param extra an secondary error code
+ * @param data optional byte array of data that may be associated with the event
+ */
+ void onEvent(ExoMediaDrm<? extends T> mediaDrm, byte[] sessionId, int event, int extra,
+ byte[] data);
+ }
+
+ /**
+ * @see android.media.MediaDrm.KeyRequest
+ */
+ interface KeyRequest {
+ byte[] getData();
+ String getDefaultUrl();
+ }
+
+ /**
+ * @see android.media.MediaDrm.ProvisionRequest
+ */
+ interface ProvisionRequest {
+ byte[] getData();
+ String getDefaultUrl();
+ }
+
+ /**
+ * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener)
+ */
+ void setOnEventListener(OnEventListener<? super T> listener);
+
+ /**
+ * @see MediaDrm#openSession()
+ */
+ byte[] openSession() throws NotProvisionedException, ResourceBusyException;
+
+ /**
+ * @see MediaDrm#closeSession(byte[])
+ */
+ void closeSession(byte[] sessionId);
+
+ /**
+ * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)
+ */
+ KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
+ HashMap<String, String> optionalParameters) throws NotProvisionedException;
+
+ /**
+ * @see MediaDrm#provideKeyResponse(byte[], byte[])
+ */
+ byte[] provideKeyResponse(byte[] scope, byte[] response)
+ throws NotProvisionedException, DeniedByServerException;
+
+ /**
+ * @see MediaDrm#getProvisionRequest()
+ */
+ ProvisionRequest getProvisionRequest();
+
+ /**
+ * @see MediaDrm#provideProvisionResponse(byte[])
+ */
+ void provideProvisionResponse(byte[] response) throws DeniedByServerException;
+
+ /**
+ * @see MediaDrm#queryKeyStatus(byte[])
+ */
+ Map<String, String> queryKeyStatus(byte[] sessionId);
+
+ /**
+ * @see MediaDrm#release()
+ */
+ void release();
+
+ /**
+ * @see MediaDrm#restoreKeys(byte[], byte[])
+ */
+ void restoreKeys(byte[] sessionId, byte[] keySetId);
+
+ /**
+ * @see MediaDrm#getPropertyString(String)
+ */
+ String getPropertyString(String propertyName);
+
+ /**
+ * @see MediaDrm#getPropertyByteArray(String)
+ */
+ byte[] getPropertyByteArray(String propertyName);
+
+ /**
+ * @see MediaDrm#setPropertyString(String, String)
+ */
+ void setPropertyString(String propertyName, String value);
+
+ /**
+ * @see MediaDrm#setPropertyByteArray(String, byte[])
+ */
+ void setPropertyByteArray(String propertyName, byte[] value);
+
+ /**
+ * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
+ *
+ * @param uuid The UUID of the crypto scheme.
+ * @param initData Opaque initialization data specific to the crypto scheme.
+ * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
+ * @throws MediaCryptoException
+ */
+ T createMediaCrypto(UUID uuid, byte[] initData) throws MediaCryptoException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaCrypto;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}.
+ */
+@TargetApi(16)
+public final class FrameworkMediaCrypto implements ExoMediaCrypto {
+
+ private final MediaCrypto mediaCrypto;
+
+ /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
+ this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
+ }
+
+ public MediaCrypto getWrappedMediaCrypto() {
+ return mediaCrypto;
+ }
+
+ @Override
+ public boolean requiresSecureDecoderComponent(String mimeType) {
+ return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -0,0 +1,169 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import android.media.UnsupportedSchemeException;
+import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}.
+ */
+@TargetApi(18)
+public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
+
+ private final MediaDrm mediaDrm;
+
+ /**
+ * Creates an instance for the specified scheme UUID.
+ *
+ * @param uuid The scheme uuid.
+ * @return The created instance.
+ * @throws UnsupportedDrmException If the DRM scheme is unsupported or cannot be instantiated.
+ */
+ public static FrameworkMediaDrm newInstance(UUID uuid) throws UnsupportedDrmException {
+ try {
+ return new FrameworkMediaDrm(uuid);
+ } catch (UnsupportedSchemeException e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);
+ } catch (Exception e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);
+ }
+ }
+
+ private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
+ this.mediaDrm = new MediaDrm(Assertions.checkNotNull(uuid));
+ }
+
+ @Override
+ public void setOnEventListener(
+ final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
+ mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() {
+ @Override
+ public void onEvent(@NonNull MediaDrm md, byte[] sessionId, int event, int extra,
+ byte[] data) {
+ listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data);
+ }
+ });
+ }
+
+ @Override
+ public byte[] openSession() throws NotProvisionedException, ResourceBusyException {
+ return mediaDrm.openSession();
+ }
+
+ @Override
+ public void closeSession(byte[] sessionId) {
+ mediaDrm.closeSession(sessionId);
+ }
+
+ @Override
+ public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
+ HashMap<String, String> optionalParameters) throws NotProvisionedException {
+ final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType,
+ optionalParameters);
+ return new KeyRequest() {
+ @Override
+ public byte[] getData() {
+ return request.getData();
+ }
+
+ @Override
+ public String getDefaultUrl() {
+ return request.getDefaultUrl();
+ }
+ };
+ }
+
+ @Override
+ public byte[] provideKeyResponse(byte[] scope, byte[] response)
+ throws NotProvisionedException, DeniedByServerException {
+ return mediaDrm.provideKeyResponse(scope, response);
+ }
+
+ @Override
+ public ProvisionRequest getProvisionRequest() {
+ final MediaDrm.ProvisionRequest provisionRequest = mediaDrm.getProvisionRequest();
+ return new ProvisionRequest() {
+ @Override
+ public byte[] getData() {
+ return provisionRequest.getData();
+ }
+
+ @Override
+ public String getDefaultUrl() {
+ return provisionRequest.getDefaultUrl();
+ }
+ };
+ }
+
+ @Override
+ public void provideProvisionResponse(byte[] response) throws DeniedByServerException {
+ mediaDrm.provideProvisionResponse(response);
+ }
+
+ @Override
+ public Map<String, String> queryKeyStatus(byte[] sessionId) {
+ return mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ public void release() {
+ mediaDrm.release();
+ }
+
+ @Override
+ public void restoreKeys(byte[] sessionId, byte[] keySetId) {
+ mediaDrm.restoreKeys(sessionId, keySetId);
+ }
+
+ @Override
+ public String getPropertyString(String propertyName) {
+ return mediaDrm.getPropertyString(propertyName);
+ }
+
+ @Override
+ public byte[] getPropertyByteArray(String propertyName) {
+ return mediaDrm.getPropertyByteArray(propertyName);
+ }
+
+ @Override
+ public void setPropertyString(String propertyName, String value) {
+ mediaDrm.setPropertyString(propertyName, value);
+ }
+
+ @Override
+ public void setPropertyByteArray(String propertyName, byte[] value) {
+ mediaDrm.setPropertyByteArray(propertyName, value);
+ }
+
+ @Override
+ public FrameworkMediaCrypto createMediaCrypto(UUID uuid, byte[] initData)
+ throws MediaCryptoException {
+ return new FrameworkMediaCrypto(new MediaCrypto(uuid, initData));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
@@ -0,0 +1,155 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.net.Uri;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link MediaDrmCallback} that makes requests using {@link HttpDataSource} instances.
+ */
+@TargetApi(18)
+public final class HttpMediaDrmCallback implements MediaDrmCallback {
+
+ private static final Map<String, String> PLAYREADY_KEY_REQUEST_PROPERTIES;
+ static {
+ PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>();
+ PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml");
+ PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction",
+ "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense");
+ }
+
+ private final HttpDataSource.Factory dataSourceFactory;
+ private final String defaultUrl;
+ private final Map<String, String> keyRequestProperties;
+
+ /**
+ * @param defaultUrl The default license URL.
+ * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ */
+ public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) {
+ this(defaultUrl, dataSourceFactory, null);
+ }
+
+ /**
+ * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request
+ * properties can be set by calling {@link #setKeyRequestProperty(String, String)}.
+ * @param defaultUrl The default license URL.
+ * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ * @param keyRequestProperties Request properties to set when making key requests, or null.
+ */
+ @Deprecated
+ public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
+ Map<String, String> keyRequestProperties) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.defaultUrl = defaultUrl;
+ this.keyRequestProperties = new HashMap<>();
+ if (keyRequestProperties != null) {
+ this.keyRequestProperties.putAll(keyRequestProperties);
+ }
+ }
+
+ /**
+ * Sets a header for key requests made by the callback.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ public void setKeyRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.put(name, value);
+ }
+ }
+
+ /**
+ * Clears a header for key requests made by the callback.
+ *
+ * @param name The name of the header field.
+ */
+ public void clearKeyRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.remove(name);
+ }
+ }
+
+ /**
+ * Clears all headers for key requests made by the callback.
+ */
+ public void clearAllKeyRequestProperties() {
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.clear();
+ }
+ }
+
+ @Override
+ public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
+ String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+ return executePost(dataSourceFactory, url, new byte[0], null);
+ }
+
+ @Override
+ public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
+ String url = request.getDefaultUrl();
+ if (TextUtils.isEmpty(url)) {
+ url = defaultUrl;
+ }
+ Map<String, String> requestProperties = new HashMap<>();
+ requestProperties.put("Content-Type", "application/octet-stream");
+ if (C.PLAYREADY_UUID.equals(uuid)) {
+ requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
+ }
+ synchronized (keyRequestProperties) {
+ requestProperties.putAll(keyRequestProperties);
+ }
+ return executePost(dataSourceFactory, url, request.getData(), requestProperties);
+ }
+
+ private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url,
+ byte[] data, Map<String, String> requestProperties) throws IOException {
+ HttpDataSource dataSource = dataSourceFactory.createDataSource();
+ if (requestProperties != null) {
+ for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
+ dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
+ }
+ }
+ DataSpec dataSpec = new DataSpec(Uri.parse(url), data, 0, 0, C.LENGTH_UNSET, null,
+ DataSpec.FLAG_ALLOW_GZIP);
+ DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+ try {
+ return Util.toByteArray(inputStream);
+ } finally {
+ Util.closeQuietly(inputStream);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/KeysExpiredException.java
@@ -0,0 +1,22 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+/**
+ * Thrown when the drm keys loaded into an open session expire.
+ */
+public final class KeysExpiredException extends Exception {
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/MediaDrmCallback.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 com.google.android.exoplayer2.drm;
+
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import java.util.UUID;
+
+/**
+ * Performs {@link ExoMediaDrm} key and provisioning requests.
+ */
+public interface MediaDrmCallback {
+
+ /**
+ * Executes a provisioning request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws Exception;
+
+ /**
+ * Executes a key request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
@@ -0,0 +1,213 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.media.MediaDrm;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Helper class to download, renew and release offline licenses.
+ */
+public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
+
+ private final ConditionVariable conditionVariable;
+ private final DefaultDrmSessionManager<T> drmSessionManager;
+ private final HandlerThread handlerThread;
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
+ * is no longer required.
+ *
+ * @param licenseUrl The default license URL.
+ * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ * @return A new instance which uses Widevine CDM.
+ * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+ * instantiated.
+ */
+ public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+ String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
+ return newWidevineInstance(
+ new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null);
+ }
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
+ * is no longer required.
+ *
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+ * @return A new instance which uses Widevine CDM.
+ * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+ * instantiated.
+ * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
+ * MediaDrmCallback, HashMap, Handler, EventListener)
+ */
+ public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+ MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters)
+ throws UnsupportedDrmException {
+ return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback,
+ optionalKeyRequestParameters);
+ }
+
+ /**
+ * Constructs an instance. Call {@link #release()} when the instance is no longer required.
+ *
+ * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+ * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
+ * MediaDrmCallback, HashMap, Handler, EventListener)
+ */
+ public OfflineLicenseHelper(ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
+ HashMap<String, String> optionalKeyRequestParameters) {
+ handlerThread = new HandlerThread("OfflineLicenseHelper");
+ handlerThread.start();
+ conditionVariable = new ConditionVariable();
+ EventListener eventListener = new EventListener() {
+ @Override
+ public void onDrmKeysLoaded() {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmSessionManagerError(Exception e) {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmKeysRestored() {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmKeysRemoved() {
+ conditionVariable.open();
+ }
+ };
+ drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback,
+ optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener);
+ }
+
+ /** Releases the helper. Should be called when the helper is no longer required. */
+ public void release() {
+ handlerThread.quit();
+ }
+
+ /**
+ * Downloads an offline license.
+ *
+ * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded.
+ * @return The key set id for the downloaded license.
+ * @throws IOException If an error occurs reading data from the stream.
+ * @throws InterruptedException If the thread has been interrupted.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws IOException,
+ InterruptedException, DrmSessionException {
+ Assertions.checkArgument(drmInitData != null);
+ return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
+ }
+
+ /**
+ * Renews an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be renewed.
+ * @return The renewed offline license key set id.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null);
+ }
+
+ /**
+ * Releases an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be released.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized void releaseLicense(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null);
+ }
+
+ /**
+ * Returns the remaining license and playback durations in seconds, for an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license.
+ * @return The remaining license and playback durations, in seconds.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ DrmSession<T> drmSession = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY,
+ offlineLicenseKeySetId, null);
+ DrmSessionException error = drmSession.getError();
+ Pair<Long, Long> licenseDurationRemainingSec =
+ WidevineUtil.getLicenseDurationRemainingSec(drmSession);
+ drmSessionManager.releaseSession(drmSession);
+ if (error != null) {
+ if (error.getCause() instanceof KeysExpiredException) {
+ return Pair.create(0L, 0L);
+ }
+ throw error;
+ }
+ return licenseDurationRemainingSec;
+ }
+
+ private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
+ DrmInitData drmInitData) throws DrmSessionException {
+ DrmSession<T> drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
+ drmInitData);
+ DrmSessionException error = drmSession.getError();
+ byte[] keySetId = drmSession.getOfflineLicenseKeySetId();
+ drmSessionManager.releaseSession(drmSession);
+ if (error != null) {
+ throw error;
+ }
+ return keySetId;
+ }
+
+ private DrmSession<T> openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
+ DrmInitData drmInitData) {
+ drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
+ conditionVariable.close();
+ DrmSession<T> drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(),
+ drmInitData);
+ // Block current thread until key loading is finished
+ conditionVariable.block();
+ return drmSession;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/UnsupportedDrmException.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Thrown when the requested DRM scheme is not supported.
+ */
+public final class UnsupportedDrmException extends Exception {
+
+ /**
+ * The reason for the exception.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR})
+ public @interface Reason {}
+ /**
+ * The requested DRM scheme is unsupported by the device.
+ */
+ public static final int REASON_UNSUPPORTED_SCHEME = 1;
+ /**
+ * There device advertises support for the requested DRM scheme, but there was an error
+ * instantiating it. The cause can be retrieved using {@link #getCause()}.
+ */
+ public static final int REASON_INSTANTIATION_ERROR = 2;
+
+ /**
+ * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ */
+ @Reason public final int reason;
+
+ /**
+ * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ */
+ public UnsupportedDrmException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ * @param cause The cause of this exception.
+ */
+ public UnsupportedDrmException(@Reason int reason, Exception cause) {
+ super(cause);
+ this.reason = reason;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/drm/WidevineUtil.java
@@ -0,0 +1,62 @@
+/*
+ * 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 com.google.android.exoplayer2.drm;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import java.util.Map;
+
+/**
+ * Utility methods for Widevine.
+ */
+public final class WidevineUtil {
+
+ /** Widevine specific key status field name for the remaining license duration, in seconds. */
+ public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
+ /** Widevine specific key status field name for the remaining playback duration, in seconds. */
+ public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
+
+ private WidevineUtil() {}
+
+ /**
+ * Returns license and playback durations remaining in seconds.
+ *
+ * @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
+ * @throws IllegalStateException If called when a session isn't opened.
+ * @param drmSession
+ */
+ public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession<?> drmSession) {
+ Map<String, String> keyStatus = drmSession.queryKeyStatus();
+ return new Pair<>(
+ getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
+ getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
+ }
+
+ private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
+ if (keyStatus != null) {
+ try {
+ String value = keyStatus.get(property);
+ if (value != null) {
+ return Long.parseLong(value);
+ }
+ } catch (NumberFormatException e) {
+ // do nothing.
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Defines chunks of samples within a media stream.
+ */
+public final class ChunkIndex implements SeekMap {
+
+ /**
+ * The number of chunks.
+ */
+ public final int length;
+
+ /**
+ * The chunk sizes, in bytes.
+ */
+ public final int[] sizes;
+
+ /**
+ * The chunk byte offsets.
+ */
+ public final long[] offsets;
+
+ /**
+ * The chunk durations, in microseconds.
+ */
+ public final long[] durationsUs;
+
+ /**
+ * The start time of each chunk, in microseconds.
+ */
+ public final long[] timesUs;
+
+ private final long durationUs;
+
+ /**
+ * @param sizes The chunk sizes, in bytes.
+ * @param offsets The chunk byte offsets.
+ * @param durationsUs The chunk durations, in microseconds.
+ * @param timesUs The start time of each chunk, in microseconds.
+ */
+ public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) {
+ this.sizes = sizes;
+ this.offsets = offsets;
+ this.durationsUs = durationsUs;
+ this.timesUs = timesUs;
+ length = sizes.length;
+ if (length > 0) {
+ durationUs = durationsUs[length - 1] + timesUs[length - 1];
+ } else {
+ durationUs = 0;
+ }
+ }
+
+ /**
+ * Obtains the index of the chunk corresponding to a given time.
+ *
+ * @param timeUs The time, in microseconds.
+ * @return The index of the corresponding chunk.
+ */
+ public int getChunkIndex(long timeUs) {
+ return Util.binarySearchFloor(timesUs, timeUs, true, true);
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return offsets[getChunkIndex(timeUs)];
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
@@ -0,0 +1,278 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * An {@link ExtractorInput} that wraps a {@link DataSource}.
+ */
+public final class DefaultExtractorInput implements ExtractorInput {
+
+ private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
+ private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
+ private static final byte[] SCRATCH_SPACE = new byte[4096];
+
+ private final DataSource dataSource;
+ private final long streamLength;
+
+ private long position;
+ private byte[] peekBuffer;
+ private int peekBufferPosition;
+ private int peekBufferLength;
+
+ /**
+ * @param dataSource The wrapped {@link DataSource}.
+ * @param position The initial position in the stream.
+ * @param length The length of the stream, or {@link C#LENGTH_UNSET} if it is unknown.
+ */
+ public DefaultExtractorInput(DataSource dataSource, long position, long length) {
+ this.dataSource = dataSource;
+ this.position = position;
+ this.streamLength = length;
+ peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
+ }
+
+ @Override
+ public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
+ int bytesRead = readFromPeekBuffer(target, offset, length);
+ if (bytesRead == 0) {
+ bytesRead = readFromDataSource(target, offset, length, 0, true);
+ }
+ commitBytesRead(bytesRead);
+ return bytesRead;
+ }
+
+ @Override
+ public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesRead = readFromPeekBuffer(target, offset, length);
+ while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) {
+ bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput);
+ }
+ commitBytesRead(bytesRead);
+ return bytesRead != C.RESULT_END_OF_INPUT;
+ }
+
+ @Override
+ public void readFully(byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ readFully(target, offset, length, false);
+ }
+
+ @Override
+ public int skip(int length) throws IOException, InterruptedException {
+ int bytesSkipped = skipFromPeekBuffer(length);
+ if (bytesSkipped == 0) {
+ bytesSkipped =
+ readFromDataSource(SCRATCH_SPACE, 0, Math.min(length, SCRATCH_SPACE.length), 0, true);
+ }
+ commitBytesRead(bytesSkipped);
+ return bytesSkipped;
+ }
+
+ @Override
+ public boolean skipFully(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesSkipped = skipFromPeekBuffer(length);
+ while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) {
+ bytesSkipped = readFromDataSource(SCRATCH_SPACE, -bytesSkipped,
+ Math.min(length, bytesSkipped + SCRATCH_SPACE.length), bytesSkipped, allowEndOfInput);
+ }
+ commitBytesRead(bytesSkipped);
+ return bytesSkipped != C.RESULT_END_OF_INPUT;
+ }
+
+ @Override
+ public void skipFully(int length) throws IOException, InterruptedException {
+ skipFully(length, false);
+ }
+
+ @Override
+ public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ if (!advancePeekPosition(length, allowEndOfInput)) {
+ return false;
+ }
+ System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length);
+ return true;
+ }
+
+ @Override
+ public void peekFully(byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ peekFully(target, offset, length, false);
+ }
+
+ @Override
+ public boolean advancePeekPosition(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ ensureSpaceForPeek(length);
+ int bytesPeeked = Math.min(peekBufferLength - peekBufferPosition, length);
+ while (bytesPeeked < length) {
+ bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked,
+ allowEndOfInput);
+ if (bytesPeeked == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ }
+ peekBufferPosition += length;
+ peekBufferLength = Math.max(peekBufferLength, peekBufferPosition);
+ return true;
+ }
+
+ @Override
+ public void advancePeekPosition(int length) throws IOException, InterruptedException {
+ advancePeekPosition(length, false);
+ }
+
+ @Override
+ public void resetPeekPosition() {
+ peekBufferPosition = 0;
+ }
+
+ @Override
+ public long getPeekPosition() {
+ return position + peekBufferPosition;
+ }
+
+ @Override
+ public long getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLength() {
+ return streamLength;
+ }
+
+ @Override
+ public <E extends Throwable> void setRetryPosition(long position, E e) throws E {
+ Assertions.checkArgument(position >= 0);
+ this.position = position;
+ throw e;
+ }
+
+ /**
+ * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the
+ * current peek position.
+ */
+ private void ensureSpaceForPeek(int length) {
+ int requiredLength = peekBufferPosition + length;
+ if (requiredLength > peekBuffer.length) {
+ int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
+ requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
+ peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
+ }
+ }
+
+ /**
+ * Skips from the peek buffer.
+ *
+ * @param length The maximum number of bytes to skip from the peek buffer.
+ * @return The number of bytes skipped.
+ */
+ private int skipFromPeekBuffer(int length) {
+ int bytesSkipped = Math.min(peekBufferLength, length);
+ updatePeekBuffer(bytesSkipped);
+ return bytesSkipped;
+ }
+
+ /**
+ * Reads from the peek buffer
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to read from the peek buffer.
+ * @return The number of bytes read.
+ */
+ private int readFromPeekBuffer(byte[] target, int offset, int length) {
+ if (peekBufferLength == 0) {
+ return 0;
+ }
+ int peekBytes = Math.min(peekBufferLength, length);
+ System.arraycopy(peekBuffer, 0, target, offset, peekBytes);
+ updatePeekBuffer(peekBytes);
+ return peekBytes;
+ }
+
+ /**
+ * Updates the peek buffer's length, position and contents after consuming data.
+ *
+ * @param bytesConsumed The number of bytes consumed from the peek buffer.
+ */
+ private void updatePeekBuffer(int bytesConsumed) {
+ peekBufferLength -= bytesConsumed;
+ peekBufferPosition = 0;
+ byte[] newPeekBuffer = peekBuffer;
+ if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
+ newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
+ }
+ System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
+ peekBuffer = newPeekBuffer;
+ }
+
+ /**
+ * Starts or continues a read from the data source.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to read from the input.
+ * @param bytesAlreadyRead The number of bytes already read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if
+ * {@code allowEndOfInput} is true and the input has ended having read no bytes.
+ * @throws EOFException If the end of input was encountered having partially satisfied the read
+ * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
+ * read and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead,
+ boolean allowEndOfInput) throws InterruptedException, IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ if (bytesAlreadyRead == 0 && allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesAlreadyRead + bytesRead;
+ }
+
+ /**
+ * Advances the position by the specified number of bytes read.
+ *
+ * @param bytesRead The number of bytes read.
+ */
+ private void commitBytesRead(int bytesRead) {
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ position += bytesRead;
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -0,0 +1,149 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
+import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
+import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
+import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import com.google.android.exoplayer2.extractor.ts.PsExtractor;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.extractor.wav.WavExtractor;
+import java.lang.reflect.Constructor;
+
+/**
+ * An {@link ExtractorsFactory} that provides an array of extractors for the following formats:
+ *
+ * <ul>
+ * <li>MP4, including M4A ({@link Mp4Extractor})</li>
+ * <li>fMP4 ({@link FragmentedMp4Extractor})</li>
+ * <li>Matroska and WebM ({@link MatroskaExtractor})</li>
+ * <li>Ogg Vorbis/FLAC ({@link OggExtractor}</li>
+ * <li>MP3 ({@link Mp3Extractor})</li>
+ * <li>AAC ({@link AdtsExtractor})</li>
+ * <li>MPEG TS ({@link TsExtractor})</li>
+ * <li>MPEG PS ({@link PsExtractor})</li>
+ * <li>FLV ({@link FlvExtractor})</li>
+ * <li>WAV ({@link WavExtractor})</li>
+ * <li>AC3 ({@link Ac3Extractor})</li>
+ * <li>FLAC (only available if the FLAC extension is built and included)</li>
+ * </ul>
+ */
+public final class DefaultExtractorsFactory implements ExtractorsFactory {
+
+ private static final Constructor<? extends Extractor> FLAC_EXTRACTOR_CONSTRUCTOR;
+ static {
+ Constructor<? extends Extractor> flacExtractorConstructor = null;
+ try {
+ flacExtractorConstructor =
+ Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
+ .asSubclass(Extractor.class).getConstructor();
+ } catch (ClassNotFoundException e) {
+ // Extractor not found.
+ } catch (NoSuchMethodException e) {
+ // Constructor not found.
+ }
+ FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor;
+ }
+
+ private @MatroskaExtractor.Flags int matroskaFlags;
+ private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags;
+ private @Mp3Extractor.Flags int mp3Flags;
+ private @DefaultTsPayloadReaderFactory.Flags int tsFlags;
+
+ /**
+ * Sets flags for {@link MatroskaExtractor} instances created by the factory.
+ *
+ * @see MatroskaExtractor#MatroskaExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMatroskaExtractorFlags(
+ @MatroskaExtractor.Flags int flags) {
+ this.matroskaFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory.
+ *
+ * @see FragmentedMp4Extractor#FragmentedMp4Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setFragmentedMp4ExtractorFlags(
+ @FragmentedMp4Extractor.Flags int flags) {
+ this.fragmentedMp4Flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link Mp3Extractor} instances created by the factory.
+ *
+ * @see Mp3Extractor#Mp3Extractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor.Flags int flags) {
+ mp3Flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances
+ * created by the factory.
+ *
+ * @see TsExtractor#TsExtractor(int)
+ * @param flags The flags to use.
+ * @return The factory, for convenience.
+ */
+ public synchronized DefaultExtractorsFactory setTsExtractorFlags(
+ @DefaultTsPayloadReaderFactory.Flags int flags) {
+ tsFlags = flags;
+ return this;
+ }
+
+ @Override
+ public synchronized Extractor[] createExtractors() {
+ Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 11 : 12];
+ extractors[0] = new MatroskaExtractor(matroskaFlags);
+ extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
+ extractors[2] = new Mp4Extractor();
+ extractors[3] = new Mp3Extractor(mp3Flags);
+ extractors[4] = new AdtsExtractor();
+ extractors[5] = new Ac3Extractor();
+ extractors[6] = new TsExtractor(tsFlags);
+ extractors[7] = new FlvExtractor();
+ extractors[8] = new OggExtractor();
+ extractors[9] = new PsExtractor();
+ extractors[10] = new WavExtractor();
+ if (FLAC_EXTRACTOR_CONSTRUCTOR != null) {
+ try {
+ extractors[11] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();
+ } catch (Exception e) {
+ // Should never happen.
+ throw new IllegalStateException("Unexpected error creating FLAC extractor", e);
+ }
+ }
+ return extractors;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
@@ -0,0 +1,997 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.upstream.Allocation;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from
+ * that queue.
+ */
+public final class DefaultTrackOutput implements TrackOutput {
+
+ /**
+ * A listener for changes to the upstream format.
+ */
+ public interface UpstreamFormatChangedListener {
+
+ /**
+ * Called on the loading thread when an upstream format change occurs.
+ *
+ * @param format The new upstream format.
+ */
+ void onUpstreamFormatChanged(Format format);
+
+ }
+
+ private static final int INITIAL_SCRATCH_SIZE = 32;
+
+ private static final int STATE_ENABLED = 0;
+ private static final int STATE_ENABLED_WRITING = 1;
+ private static final int STATE_DISABLED = 2;
+
+ private final Allocator allocator;
+ private final int allocationLength;
+
+ private final InfoQueue infoQueue;
+ private final LinkedBlockingDeque<Allocation> dataQueue;
+ private final BufferExtrasHolder extrasHolder;
+ private final ParsableByteArray scratch;
+ private final AtomicInteger state;
+
+ // Accessed only by the consuming thread.
+ private long totalBytesDropped;
+ private Format downstreamFormat;
+
+ // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
+ private boolean pendingFormatAdjustment;
+ private Format lastUnadjustedFormat;
+ private long sampleOffsetUs;
+ private long totalBytesWritten;
+ private Allocation lastAllocation;
+ private int lastAllocationOffset;
+ private boolean pendingSplice;
+ private UpstreamFormatChangedListener upstreamFormatChangeListener;
+
+ /**
+ * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
+ */
+ public DefaultTrackOutput(Allocator allocator) {
+ this.allocator = allocator;
+ allocationLength = allocator.getIndividualAllocationLength();
+ infoQueue = new InfoQueue();
+ dataQueue = new LinkedBlockingDeque<>();
+ extrasHolder = new BufferExtrasHolder();
+ scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
+ state = new AtomicInteger();
+ lastAllocationOffset = allocationLength;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /**
+ * Resets the output.
+ *
+ * @param enable Whether the output should be enabled. False if it should be disabled.
+ */
+ public void reset(boolean enable) {
+ int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED);
+ clearSampleData();
+ infoQueue.resetLargestParsedTimestamps();
+ if (previousState == STATE_DISABLED) {
+ downstreamFormat = null;
+ }
+ }
+
+ /**
+ * Sets a source identifier for subsequent samples.
+ *
+ * @param sourceId The source identifier.
+ */
+ public void sourceId(int sourceId) {
+ infoQueue.sourceId(sourceId);
+ }
+
+ /**
+ * Indicates that samples subsequently queued to the buffer should be spliced into those already
+ * queued.
+ */
+ public void splice() {
+ pendingSplice = true;
+ }
+
+ /**
+ * Returns the current absolute write index.
+ */
+ public int getWriteIndex() {
+ return infoQueue.getWriteIndex();
+ }
+
+ /**
+ * Discards samples from the write side of the buffer.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded.
+ */
+ public void discardUpstreamSamples(int discardFromIndex) {
+ totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex);
+ dropUpstreamFrom(totalBytesWritten);
+ }
+
+ /**
+ * Discards data from the write side of the buffer. Data is discarded from the specified absolute
+ * position. Any allocations that are fully discarded are returned to the allocator.
+ *
+ * @param absolutePosition The absolute position (inclusive) from which to discard data.
+ */
+ private void dropUpstreamFrom(long absolutePosition) {
+ int relativePosition = (int) (absolutePosition - totalBytesDropped);
+ // Calculate the index of the allocation containing the position, and the offset within it.
+ int allocationIndex = relativePosition / allocationLength;
+ int allocationOffset = relativePosition % allocationLength;
+ // We want to discard any allocations after the one at allocationIdnex.
+ int allocationDiscardCount = dataQueue.size() - allocationIndex - 1;
+ if (allocationOffset == 0) {
+ // If the allocation at allocationIndex is empty, we should discard that one too.
+ allocationDiscardCount++;
+ }
+ // Discard the allocations.
+ for (int i = 0; i < allocationDiscardCount; i++) {
+ allocator.release(dataQueue.removeLast());
+ }
+ // Update lastAllocation and lastAllocationOffset to reflect the new position.
+ lastAllocation = dataQueue.peekLast();
+ lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset;
+ }
+
+ // Called by the consuming thread.
+
+ /**
+ * Disables buffering of sample data and metadata.
+ */
+ public void disable() {
+ if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) {
+ clearSampleData();
+ }
+ }
+
+ /**
+ * Returns whether the buffer is empty.
+ */
+ public boolean isEmpty() {
+ return infoQueue.isEmpty();
+ }
+
+ /**
+ * Returns the current absolute read index.
+ */
+ public int getReadIndex() {
+ return infoQueue.getReadIndex();
+ }
+
+ /**
+ * Peeks the source id of the next sample, or the current upstream source id if the buffer is
+ * empty.
+ *
+ * @return The source id.
+ */
+ public int peekSourceId() {
+ return infoQueue.peekSourceId();
+ }
+
+ /**
+ * Returns the upstream {@link Format} in which samples are being queued.
+ */
+ public Format getUpstreamFormat() {
+ return infoQueue.getUpstreamFormat();
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+ * <p>
+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public long getLargestQueuedTimestampUs() {
+ return infoQueue.getLargestQueuedTimestampUs();
+ }
+
+ /**
+ * Skips all samples currently in the buffer.
+ */
+ public void skipAll() {
+ long nextOffset = infoQueue.skipAll();
+ if (nextOffset != C.POSITION_UNSET) {
+ dropDownstreamTo(nextOffset);
+ }
+ }
+
+ /**
+ * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
+ * contains a keyframe with a timestamp of {@code timeUs} or earlier. If
+ * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
+ * falls within the buffer.
+ *
+ * @param timeUs The seek time.
+ * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
+ * of the buffer.
+ * @return Whether the skip was successful.
+ */
+ public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
+ long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
+ if (nextOffset == C.POSITION_UNSET) {
+ return false;
+ }
+ dropDownstreamTo(nextOffset);
+ return true;
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
+ * be set if the buffer's timestamp is less than this value.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
+ boolean loadingFinished, long decodeOnlyUntilUs) {
+ int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ downstreamFormat, extrasHolder);
+ switch (result) {
+ case C.RESULT_FORMAT_READ:
+ downstreamFormat = formatHolder.format;
+ return C.RESULT_FORMAT_READ;
+ case C.RESULT_BUFFER_READ:
+ if (!buffer.isEndOfStream()) {
+ if (buffer.timeUs < decodeOnlyUntilUs) {
+ buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ }
+ // Read encryption data if the sample is encrypted.
+ if (buffer.isEncrypted()) {
+ readEncryptionData(buffer, extrasHolder);
+ }
+ // Write the sample data into the holder.
+ buffer.ensureSpaceForWrite(extrasHolder.size);
+ readData(extrasHolder.offset, buffer.data, extrasHolder.size);
+ // Advance the read head.
+ dropDownstreamTo(extrasHolder.nextOffset);
+ }
+ return C.RESULT_BUFFER_READ;
+ case C.RESULT_NOTHING_READ:
+ return C.RESULT_NOTHING_READ;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Reads encryption data for the current sample.
+ * <p>
+ * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and
+ * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The
+ * same value is added to {@link BufferExtrasHolder#offset}.
+ *
+ * @param buffer The buffer into which the encryption data should be written.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ */
+ private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) {
+ long offset = extrasHolder.offset;
+
+ // Read the signal byte.
+ scratch.reset(1);
+ readData(offset, scratch.data, 1);
+ offset++;
+ byte signalByte = scratch.data[0];
+ boolean subsampleEncryption = (signalByte & 0x80) != 0;
+ int ivSize = signalByte & 0x7F;
+
+ // Read the initialization vector.
+ if (buffer.cryptoInfo.iv == null) {
+ buffer.cryptoInfo.iv = new byte[16];
+ }
+ readData(offset, buffer.cryptoInfo.iv, ivSize);
+ offset += ivSize;
+
+ // Read the subsample count, if present.
+ int subsampleCount;
+ if (subsampleEncryption) {
+ scratch.reset(2);
+ readData(offset, scratch.data, 2);
+ offset += 2;
+ subsampleCount = scratch.readUnsignedShort();
+ } else {
+ subsampleCount = 1;
+ }
+
+ // Write the clear and encrypted subsample sizes.
+ int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
+ if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+ clearDataSizes = new int[subsampleCount];
+ }
+ int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
+ if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+ encryptedDataSizes = new int[subsampleCount];
+ }
+ if (subsampleEncryption) {
+ int subsampleDataLength = 6 * subsampleCount;
+ scratch.reset(subsampleDataLength);
+ readData(offset, scratch.data, subsampleDataLength);
+ offset += subsampleDataLength;
+ scratch.setPosition(0);
+ for (int i = 0; i < subsampleCount; i++) {
+ clearDataSizes[i] = scratch.readUnsignedShort();
+ encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
+ }
+ } else {
+ clearDataSizes[0] = 0;
+ encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
+ }
+
+ // Populate the cryptoInfo.
+ buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
+ extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
+
+ // Adjust the offset and size to take into account the bytes read.
+ int bytesRead = (int) (offset - extrasHolder.offset);
+ extrasHolder.offset += bytesRead;
+ extrasHolder.size -= bytesRead;
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The buffer into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, ByteBuffer target, int length) {
+ int remaining = length;
+ while (remaining > 0) {
+ dropDownstreamTo(absolutePosition);
+ int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
+ int toCopy = Math.min(remaining, allocationLength - positionInAllocation);
+ Allocation allocation = dataQueue.peek();
+ target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy);
+ absolutePosition += toCopy;
+ remaining -= toCopy;
+ }
+ }
+
+ /**
+ * Reads data from the front of the rolling buffer.
+ *
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The array into which data should be written.
+ * @param length The number of bytes to read.
+ */
+ private void readData(long absolutePosition, byte[] target, int length) {
+ int bytesRead = 0;
+ while (bytesRead < length) {
+ dropDownstreamTo(absolutePosition);
+ int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
+ int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation);
+ Allocation allocation = dataQueue.peek();
+ System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target,
+ bytesRead, toCopy);
+ absolutePosition += toCopy;
+ bytesRead += toCopy;
+ }
+ }
+
+ /**
+ * Discard any allocations that hold data prior to the specified absolute position, returning
+ * them to the allocator.
+ *
+ * @param absolutePosition The absolute position up to which allocations can be discarded.
+ */
+ private void dropDownstreamTo(long absolutePosition) {
+ int relativePosition = (int) (absolutePosition - totalBytesDropped);
+ int allocationIndex = relativePosition / allocationLength;
+ for (int i = 0; i < allocationIndex; i++) {
+ allocator.release(dataQueue.remove());
+ totalBytesDropped += allocationLength;
+ }
+ }
+
+ // Called by the loading thread.
+
+ /**
+ * Sets a listener to be notified of changes to the upstream format.
+ *
+ * @param listener The listener.
+ */
+ public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
+ upstreamFormatChangeListener = listener;
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
+ * subsequently queued to the buffer.
+ *
+ * @param sampleOffsetUs The timestamp offset in microseconds.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ if (this.sampleOffsetUs != sampleOffsetUs) {
+ this.sampleOffsetUs = sampleOffsetUs;
+ pendingFormatAdjustment = true;
+ }
+ }
+
+ @Override
+ public void format(Format format) {
+ Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
+ boolean formatChanged = infoQueue.format(adjustedFormat);
+ lastUnadjustedFormat = format;
+ pendingFormatAdjustment = false;
+ if (upstreamFormatChangeListener != null && formatChanged) {
+ upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
+ }
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ if (!startWriteOperation()) {
+ int bytesSkipped = input.skip(length);
+ if (bytesSkipped == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesSkipped;
+ }
+ try {
+ length = prepareForAppend(length);
+ int bytesAppended = input.read(lastAllocation.data,
+ lastAllocation.translateOffset(lastAllocationOffset), length);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ lastAllocationOffset += bytesAppended;
+ totalBytesWritten += bytesAppended;
+ return bytesAppended;
+ } finally {
+ endWriteOperation();
+ }
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray buffer, int length) {
+ if (!startWriteOperation()) {
+ buffer.skipBytes(length);
+ return;
+ }
+ while (length > 0) {
+ int thisAppendLength = prepareForAppend(length);
+ buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset),
+ thisAppendLength);
+ lastAllocationOffset += thisAppendLength;
+ totalBytesWritten += thisAppendLength;
+ length -= thisAppendLength;
+ }
+ endWriteOperation();
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ byte[] encryptionKey) {
+ if (pendingFormatAdjustment) {
+ format(lastUnadjustedFormat);
+ }
+ if (!startWriteOperation()) {
+ infoQueue.commitSampleTimestamp(timeUs);
+ return;
+ }
+ try {
+ if (pendingSplice) {
+ if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) {
+ return;
+ }
+ pendingSplice = false;
+ }
+ timeUs += sampleOffsetUs;
+ long absoluteOffset = totalBytesWritten - size - offset;
+ infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey);
+ } finally {
+ endWriteOperation();
+ }
+ }
+
+ // Private methods.
+
+ private boolean startWriteOperation() {
+ return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING);
+ }
+
+ private void endWriteOperation() {
+ if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) {
+ clearSampleData();
+ }
+ }
+
+ private void clearSampleData() {
+ infoQueue.clearSampleData();
+ allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()]));
+ dataQueue.clear();
+ allocator.trim();
+ totalBytesDropped = 0;
+ totalBytesWritten = 0;
+ lastAllocation = null;
+ lastAllocationOffset = allocationLength;
+ }
+
+ /**
+ * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the
+ * number of bytes that can actually be appended.
+ */
+ private int prepareForAppend(int length) {
+ if (lastAllocationOffset == allocationLength) {
+ lastAllocationOffset = 0;
+ lastAllocation = allocator.allocate();
+ dataQueue.add(lastAllocation);
+ }
+ return Math.min(length, allocationLength - lastAllocationOffset);
+ }
+
+ /**
+ * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
+ *
+ * @param format The {@link Format} to adjust.
+ * @param sampleOffsetUs The offset to apply.
+ * @return The adjusted {@link Format}.
+ */
+ private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
+ if (format == null) {
+ return null;
+ }
+ if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+ format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
+ }
+ return format;
+ }
+
+ /**
+ * Holds information about the samples in the rolling buffer.
+ */
+ private static final class InfoQueue {
+
+ private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
+
+ private int capacity;
+
+ private int[] sourceIds;
+ private long[] offsets;
+ private int[] sizes;
+ private int[] flags;
+ private long[] timesUs;
+ private byte[][] encryptionKeys;
+ private Format[] formats;
+
+ private int queueSize;
+ private int absoluteReadIndex;
+ private int relativeReadIndex;
+ private int relativeWriteIndex;
+
+ private long largestDequeuedTimestampUs;
+ private long largestQueuedTimestampUs;
+ private boolean upstreamKeyframeRequired;
+ private boolean upstreamFormatRequired;
+ private Format upstreamFormat;
+ private int upstreamSourceId;
+
+ public InfoQueue() {
+ capacity = SAMPLE_CAPACITY_INCREMENT;
+ sourceIds = new int[capacity];
+ offsets = new long[capacity];
+ timesUs = new long[capacity];
+ flags = new int[capacity];
+ sizes = new int[capacity];
+ encryptionKeys = new byte[capacity][];
+ formats = new Format[capacity];
+ largestDequeuedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ upstreamFormatRequired = true;
+ upstreamKeyframeRequired = true;
+ }
+
+ public void clearSampleData() {
+ absoluteReadIndex = 0;
+ relativeReadIndex = 0;
+ relativeWriteIndex = 0;
+ queueSize = 0;
+ upstreamKeyframeRequired = true;
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ public void resetLargestParsedTimestamps() {
+ largestDequeuedTimestampUs = Long.MIN_VALUE;
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ }
+
+ /**
+ * Returns the current absolute write index.
+ */
+ public int getWriteIndex() {
+ return absoluteReadIndex + queueSize;
+ }
+
+ /**
+ * Discards samples from the write side of the buffer.
+ *
+ * @param discardFromIndex The absolute index of the first sample to be discarded.
+ * @return The reduced total number of bytes written, after the samples have been discarded.
+ */
+ public long discardUpstreamSamples(int discardFromIndex) {
+ int discardCount = getWriteIndex() - discardFromIndex;
+ Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize);
+
+ if (discardCount == 0) {
+ if (absoluteReadIndex == 0) {
+ // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue.
+ return 0;
+ }
+ int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1;
+ return offsets[lastWriteIndex] + sizes[lastWriteIndex];
+ }
+
+ queueSize -= discardCount;
+ relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity;
+ // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are
+ // always less than the timestamp of the keyframe itself, and of subsequent frames.
+ largestQueuedTimestampUs = Long.MIN_VALUE;
+ for (int i = queueSize - 1; i >= 0; i--) {
+ int sampleIndex = (relativeReadIndex + i) % capacity;
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]);
+ if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ break;
+ }
+ }
+ return offsets[relativeWriteIndex];
+ }
+
+ public void sourceId(int sourceId) {
+ upstreamSourceId = sourceId;
+ }
+
+ // Called by the consuming thread.
+
+ /**
+ * Returns the current absolute read index.
+ */
+ public int getReadIndex() {
+ return absoluteReadIndex;
+ }
+
+ /**
+ * Peeks the source id of the next sample, or the current upstream source id if the queue is
+ * empty.
+ */
+ public int peekSourceId() {
+ return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex];
+ }
+
+ /**
+ * Returns whether the queue is empty.
+ */
+ public synchronized boolean isEmpty() {
+ return queueSize == 0;
+ }
+
+ /**
+ * Returns the upstream {@link Format} in which samples are being queued.
+ */
+ public synchronized Format getUpstreamFormat() {
+ return upstreamFormatRequired ? null : upstreamFormat;
+ }
+
+ /**
+ * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+ * <p>
+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
+ * considered as having been queued. Samples that were dequeued from the front of the queue are
+ * considered as having been queued.
+ *
+ * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
+ * samples have been queued.
+ */
+ public synchronized long getLargestQueuedTimestampUs() {
+ return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs);
+ }
+
+ /**
+ * Attempts to read from the queue.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If a sample is read then the buffer is populated with information
+ * about the sample, but not its data. The size and absolute position of the data in the
+ * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
+ * and the absolute position of the first byte that may still be required after the current
+ * sample has been read. May be null if the caller requires that the format of the stream be
+ * read even if it's not changing.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even
+ * if it's not changing. A sample will never be read if set to true, however it is still
+ * possible for the end of stream or nothing to be read.
+ * @param loadingFinished True if an empty queue should be considered the end of the stream.
+ * @param downstreamFormat The current downstream {@link Format}. If the format of the next
+ * sample is different to the current downstream format then a format will be read.
+ * @param extrasHolder The holder into which extra sample information should be written.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
+ * or {@link C#RESULT_BUFFER_READ}.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
+ BufferExtrasHolder extrasHolder) {
+ if (queueSize == 0) {
+ if (loadingFinished) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else if (upstreamFormat != null
+ && (formatRequired || upstreamFormat != downstreamFormat)) {
+ formatHolder.format = upstreamFormat;
+ return C.RESULT_FORMAT_READ;
+ } else {
+ return C.RESULT_NOTHING_READ;
+ }
+ }
+
+ if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
+ formatHolder.format = formats[relativeReadIndex];
+ return C.RESULT_FORMAT_READ;
+ }
+
+ if (buffer.isFlagsOnly()) {
+ return C.RESULT_NOTHING_READ;
+ }
+
+ buffer.timeUs = timesUs[relativeReadIndex];
+ buffer.setFlags(flags[relativeReadIndex]);
+ extrasHolder.size = sizes[relativeReadIndex];
+ extrasHolder.offset = offsets[relativeReadIndex];
+ extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
+
+ largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs);
+ queueSize--;
+ relativeReadIndex++;
+ absoluteReadIndex++;
+ if (relativeReadIndex == capacity) {
+ // Wrap around.
+ relativeReadIndex = 0;
+ }
+
+ extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex]
+ : extrasHolder.offset + extrasHolder.size;
+ return C.RESULT_BUFFER_READ;
+ }
+
+ /**
+ * Skips all samples in the buffer.
+ *
+ * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no
+ * dropping of data is required.
+ */
+ public synchronized long skipAll() {
+ if (queueSize == 0) {
+ return C.POSITION_UNSET;
+ }
+
+ int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity;
+ relativeReadIndex = (relativeReadIndex + queueSize) % capacity;
+ absoluteReadIndex += queueSize;
+ queueSize = 0;
+ return offsets[lastSampleIndex] + sizes[lastSampleIndex];
+ }
+
+ /**
+ * Attempts to locate the keyframe before or at the specified time. If
+ * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
+ * falls within the buffer.
+ *
+ * @param timeUs The seek time.
+ * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
+ * of the buffer.
+ * @return The offset of the keyframe's data if the keyframe was present.
+ * {@link C#POSITION_UNSET} otherwise.
+ */
+ public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
+ if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
+ return C.POSITION_UNSET;
+ }
+
+ if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
+ return C.POSITION_UNSET;
+ }
+
+ // This could be optimized to use a binary search, however in practice callers to this method
+ // often pass times near to the start of the buffer. Hence it's unclear whether switching to
+ // a binary search would yield any real benefit.
+ int sampleCount = 0;
+ int sampleCountToKeyframe = -1;
+ int searchIndex = relativeReadIndex;
+ while (searchIndex != relativeWriteIndex) {
+ if (timesUs[searchIndex] > timeUs) {
+ // We've gone too far.
+ break;
+ } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ // We've found a keyframe, and we're still before the seek position.
+ sampleCountToKeyframe = sampleCount;
+ }
+ searchIndex = (searchIndex + 1) % capacity;
+ sampleCount++;
+ }
+
+ if (sampleCountToKeyframe == -1) {
+ return C.POSITION_UNSET;
+ }
+
+ relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity;
+ absoluteReadIndex += sampleCountToKeyframe;
+ queueSize -= sampleCountToKeyframe;
+ return offsets[relativeReadIndex];
+ }
+
+ // Called by the loading thread.
+
+ public synchronized boolean format(Format format) {
+ if (format == null) {
+ upstreamFormatRequired = true;
+ return false;
+ }
+ upstreamFormatRequired = false;
+ if (Util.areEqual(format, upstreamFormat)) {
+ // Suppress changes between equal formats so we can use referential equality in readData.
+ return false;
+ } else {
+ upstreamFormat = format;
+ return true;
+ }
+ }
+
+ public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
+ int size, byte[] encryptionKey) {
+ if (upstreamKeyframeRequired) {
+ if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+ return;
+ }
+ upstreamKeyframeRequired = false;
+ }
+ Assertions.checkState(!upstreamFormatRequired);
+ commitSampleTimestamp(timeUs);
+ timesUs[relativeWriteIndex] = timeUs;
+ offsets[relativeWriteIndex] = offset;
+ sizes[relativeWriteIndex] = size;
+ flags[relativeWriteIndex] = sampleFlags;
+ encryptionKeys[relativeWriteIndex] = encryptionKey;
+ formats[relativeWriteIndex] = upstreamFormat;
+ sourceIds[relativeWriteIndex] = upstreamSourceId;
+ // Increment the write index.
+ queueSize++;
+ if (queueSize == capacity) {
+ // Increase the capacity.
+ int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
+ int[] newSourceIds = new int[newCapacity];
+ long[] newOffsets = new long[newCapacity];
+ long[] newTimesUs = new long[newCapacity];
+ int[] newFlags = new int[newCapacity];
+ int[] newSizes = new int[newCapacity];
+ byte[][] newEncryptionKeys = new byte[newCapacity][];
+ Format[] newFormats = new Format[newCapacity];
+ int beforeWrap = capacity - relativeReadIndex;
+ System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap);
+ System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap);
+ System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap);
+ System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap);
+ System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap);
+ System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap);
+ System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap);
+ int afterWrap = relativeReadIndex;
+ System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
+ System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
+ System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
+ System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
+ System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap);
+ System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
+ System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
+ offsets = newOffsets;
+ timesUs = newTimesUs;
+ flags = newFlags;
+ sizes = newSizes;
+ encryptionKeys = newEncryptionKeys;
+ formats = newFormats;
+ sourceIds = newSourceIds;
+ relativeReadIndex = 0;
+ relativeWriteIndex = capacity;
+ queueSize = capacity;
+ capacity = newCapacity;
+ } else {
+ relativeWriteIndex++;
+ if (relativeWriteIndex == capacity) {
+ // Wrap around.
+ relativeWriteIndex = 0;
+ }
+ }
+ }
+
+ public synchronized void commitSampleTimestamp(long timeUs) {
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
+ }
+
+ /**
+ * Attempts to discard samples from the tail of the queue to allow samples starting from the
+ * specified timestamp to be spliced in.
+ *
+ * @param timeUs The timestamp at which the splice occurs.
+ * @return Whether the splice was successful.
+ */
+ public synchronized boolean attemptSplice(long timeUs) {
+ if (largestDequeuedTimestampUs >= timeUs) {
+ return false;
+ }
+ int retainCount = queueSize;
+ while (retainCount > 0
+ && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) {
+ retainCount--;
+ }
+ discardUpstreamSamples(absoluteReadIndex + retainCount);
+ return true;
+ }
+
+ }
+
+ /**
+ * Holds additional buffer information not held by {@link DecoderInputBuffer}.
+ */
+ private static final class BufferExtrasHolder {
+
+ public int size;
+ public long offset;
+ public long nextOffset;
+ public byte[] encryptionKeyId;
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * A dummy {@link TrackOutput} implementation.
+ */
+public final class DummyTrackOutput implements TrackOutput {
+
+ @Override
+ public void format(Format format) {
+ // Do nothing.
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ int bytesSkipped = input.skip(length);
+ if (bytesSkipped == C.RESULT_END_OF_INPUT) {
+ if (allowEndOfInput) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ throw new EOFException();
+ }
+ return bytesSkipped;
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray data, int length) {
+ data.skipBytes(length);
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ byte[] encryptionKey) {
+ // Do nothing.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/Extractor.java
@@ -0,0 +1,106 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import java.io.IOException;
+
+/**
+ * Extracts media data from a container format.
+ */
+public interface Extractor {
+
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
+ * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data
+ * continuing from the position in the stream reached by the returning call.
+ */
+ int RESULT_CONTINUE = 0;
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
+ * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting
+ * from a specified position in the stream.
+ */
+ int RESULT_SEEK = 1;
+ /**
+ * Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the
+ * {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}.
+ */
+ int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
+
+ /**
+ * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must
+ * provide data from the start of the stream.
+ * <p>
+ * If {@code true} is returned, the {@code input}'s reading position may have been modified.
+ * Otherwise, only its peek position may have been modified.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked/read.
+ * @return Whether this extractor can read the provided input.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ boolean sniff(ExtractorInput input) throws IOException, InterruptedException;
+
+ /**
+ * Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
+ *
+ * @param output An {@link ExtractorOutput} to receive extracted data.
+ */
+ void init(ExtractorOutput output);
+
+ /**
+ * Extracts data read from a provided {@link ExtractorInput}.
+ * <p>
+ * A single call to this method will block until some progress has been made, but will not block
+ * for longer than this. Hence each call will consume only a small amount of input data.
+ * <p>
+ * In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the
+ * {@link ExtractorInput} passed to the next read is required to provide data continuing from the
+ * position in the stream reached by the returning call. If the extractor requires data to be
+ * provided from a different position, then that position is set in {@code seekPosition} and
+ * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the
+ * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
+ * position of the required data.
+ * @return One of the {@code RESULT_} values defined in this interface.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException;
+
+ /**
+ * Notifies the extractor that a seek has occurred.
+ * <p>
+ * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of
+ * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from {@code
+ * position} in the stream. Valid random access positions are the start of the stream and
+ * positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}.
+ *
+ * @param position The byte offset in the stream from which data will be provided.
+ * @param timeUs The seek time in microseconds.
+ */
+ void seek(long position, long timeUs);
+
+ /**
+ * Releases all kept resources.
+ */
+ void release();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ExtractorInput.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Provides data to be consumed by an {@link Extractor}.
+ */
+public interface ExtractorInput {
+
+ /**
+ * Reads up to {@code length} bytes from the input and resets the peek position.
+ * <p>
+ * This method blocks until at least one byte of data can be read, the end of the input is
+ * detected, or an exception is thrown.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The maximum number of bytes to read from the input.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int read(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full.
+ * <p>
+ * If the end of the input is found having read no data, then behavior is dependent on
+ * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.
+ * Otherwise an {@link EOFException} is thrown.
+ * <p>
+ * Encountering the end of input having partially satisfied the read is always considered an
+ * error, and will result in an {@link EOFException} being thrown.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@code false} being returned. False if it should be
+ * considered an error, causing an {@link EOFException} to be thrown.
+ * @return True if the read was successful. False if the end of the input was encountered having
+ * read no data.
+ * @throws EOFException If the end of input was encountered having partially satisfied the read
+ * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were
+ * read and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Equivalent to {@code readFully(target, offset, length, false)}.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to read from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read.
+ *
+ * @param length The maximum number of bytes to skip from the input.
+ * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ int skip(int length) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read.
+ *
+ * @param length The number of bytes to skip from the input.
+ * @param allowEndOfInput True if encountering the end of the input having skipped no data is
+ * allowed, and should result in {@code false} being returned. False if it should be
+ * considered an error, causing an {@link EOFException} to be thrown.
+ * @return True if the skip was successful. False if the end of the input was encountered having
+ * skipped no data.
+ * @throws EOFException If the end of input was encountered having partially satisfied the skip
+ * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were
+ * skipped and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException;
+
+ /**
+ * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read.
+ * <p>
+ * Encountering the end of input is always considered an error, and will result in an
+ * {@link EOFException} being thrown.
+ *
+ * @param length The number of bytes to skip from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void skipFully(int length) throws IOException, InterruptedException;
+
+ /**
+ * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index
+ * {@code offset}. The current read position is left unchanged.
+ * <p>
+ * If the end of the input is found having peeked no data, then behavior is dependent on
+ * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.
+ * Otherwise an {@link EOFException} is thrown.
+ * <p>
+ * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read
+ * position, so the caller can peek the same data again. Reading or skipping also resets the peek
+ * position.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to peek from the input.
+ * @param allowEndOfInput True if encountering the end of the input having peeked no data is
+ * allowed, and should result in {@code false} being returned. False if it should be
+ * considered an error, causing an {@link EOFException} to be thrown.
+ * @return True if the peek was successful. False if the end of the input was encountered having
+ * peeked no data.
+ * @throws EOFException If the end of input was encountered having partially satisfied the peek
+ * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were
+ * peeked and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index
+ * {@code offset}. The current read position is left unchanged.
+ * <p>
+ * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read
+ * position, so the caller can peek the same data again. Reading and skipping also reset the peek
+ * position.
+ *
+ * @param target A target array into which data should be written.
+ * @param offset The offset into the target array at which to write.
+ * @param length The number of bytes to peek from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException;
+
+ /**
+ * Advances the peek position by {@code length} bytes.
+ * <p>
+ * If the end of the input is encountered before advancing the peek position, then behavior is
+ * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is
+ * returned. Otherwise an {@link EOFException} is thrown.
+ *
+ * @param length The number of bytes by which to advance the peek position.
+ * @param allowEndOfInput True if encountering the end of the input before advancing is allowed,
+ * and should result in {@code false} being returned. False if it should be considered an
+ * error, causing an {@link EOFException} to be thrown.
+ * @return True if advancing the peek position was successful. False if the end of the input was
+ * encountered before the peek position could be advanced.
+ * @throws EOFException If the end of input was encountered having partially advanced (i.e. having
+ * advanced by at least one byte, but fewer than {@code length}), or if the end of input was
+ * encountered before advancing and {@code allowEndOfInput} is false.
+ * @throws IOException If an error occurs advancing the peek position.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean advancePeekPosition(int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Advances the peek position by {@code length} bytes.
+ *
+ * @param length The number of bytes to peek from the input.
+ * @throws EOFException If the end of input was encountered.
+ * @throws IOException If an error occurs peeking from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void advancePeekPosition(int length) throws IOException, InterruptedException;
+
+ /**
+ * Resets the peek position to equal the current read position.
+ */
+ void resetPeekPosition();
+
+ /**
+ * Returns the current peek position (byte offset) in the stream.
+ *
+ * @return The peek position (byte offset) in the stream.
+ */
+ long getPeekPosition();
+
+ /**
+ * Returns the current read position (byte offset) in the stream.
+ *
+ * @return The read position (byte offset) in the stream.
+ */
+ long getPosition();
+
+ /**
+ * Returns the length of the source stream, or {@link C#LENGTH_UNSET} if it is unknown.
+ *
+ * @return The length of the source stream, or {@link C#LENGTH_UNSET}.
+ */
+ long getLength();
+
+ /**
+ * Called when reading fails and the required retry position is different from the last position.
+ * After setting the retry position it throws the given {@link Throwable}.
+ *
+ * @param <E> Type of {@link Throwable} to be thrown.
+ * @param position The required retry position.
+ * @param e {@link Throwable} to be thrown.
+ * @throws E The given {@link Throwable} object.
+ */
+ <E extends Throwable> void setRetryPosition(long position, E e) throws E;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+/**
+ * Receives stream level data extracted by an {@link Extractor}.
+ */
+public interface ExtractorOutput {
+
+ /**
+ * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
+ * <p>
+ * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
+ *
+ * @param id A track identifier.
+ * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}
+ * {@code TRACK_TYPE_*} constants.
+ * @return The {@link TrackOutput} for the given track identifier.
+ */
+ TrackOutput track(int id, int type);
+
+ /**
+ * Called when all tracks have been identified, meaning no new {@code trackId} values will be
+ * passed to {@link #track(int, int)}.
+ */
+ void endTracks();
+
+ /**
+ * Called when a {@link SeekMap} has been extracted from the stream.
+ *
+ * @param seekMap The extracted {@link SeekMap}.
+ */
+ void seekMap(SeekMap seekMap);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+/**
+ * Factory for arrays of {@link Extractor}s.
+ */
+public interface ExtractorsFactory {
+
+ /**
+ * Returns an array of new {@link Extractor} instances.
+ */
+ Extractor[] createExtractors();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
@@ -0,0 +1,138 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.CommentFrame;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Holder for gapless playback information.
+ */
+public final class GaplessInfoHolder {
+
+ /**
+ * A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed
+ * to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback
+ * information are decoded.
+ */
+ public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() {
+ @Override
+ public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) {
+ return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2);
+ }
+ };
+
+ private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
+ private static final Pattern GAPLESS_COMMENT_PATTERN =
+ Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
+
+ /**
+ * The number of samples to trim from the start of the decoded audio stream, or
+ * {@link Format#NO_VALUE} if not set.
+ */
+ public int encoderDelay;
+
+ /**
+ * The number of samples to trim from the end of the decoded audio stream, or
+ * {@link Format#NO_VALUE} if not set.
+ */
+ public int encoderPadding;
+
+ /**
+ * Creates a new holder for gapless playback information.
+ */
+ public GaplessInfoHolder() {
+ encoderDelay = Format.NO_VALUE;
+ encoderPadding = Format.NO_VALUE;
+ }
+
+ /**
+ * Populates the holder with data from an MP3 Xing header, if valid and non-zero.
+ *
+ * @param value The 24-bit value to decode.
+ * @return Whether the holder was populated.
+ */
+ public boolean setFromXingHeaderValue(int value) {
+ int encoderDelay = value >> 12;
+ int encoderPadding = value & 0x0FFF;
+ if (encoderDelay > 0 || encoderPadding > 0) {
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Populates the holder with data parsed from ID3 {@link Metadata}.
+ *
+ * @param metadata The metadata from which to parse the gapless information.
+ * @return Whether the holder was populated.
+ */
+ public boolean setFromMetadata(Metadata metadata) {
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof CommentFrame) {
+ CommentFrame commentFrame = (CommentFrame) entry;
+ if (setFromComment(commentFrame.description, commentFrame.text)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
+ * or MPEG 4 user data), if valid and non-zero.
+ *
+ * @param name The comment's identifier.
+ * @param data The comment's payload data.
+ * @return Whether the holder was populated.
+ */
+ private boolean setFromComment(String name, String data) {
+ if (!GAPLESS_COMMENT_ID.equals(name)) {
+ return false;
+ }
+ Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
+ if (matcher.find()) {
+ try {
+ int encoderDelay = Integer.parseInt(matcher.group(1), 16);
+ int encoderPadding = Integer.parseInt(matcher.group(2), 16);
+ if (encoderDelay > 0 || encoderPadding > 0) {
+ this.encoderDelay = encoderDelay;
+ this.encoderPadding = encoderPadding;
+ return true;
+ }
+ } catch (NumberFormatException e) {
+ // Ignore incorrectly formatted comments.
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether {@link #encoderDelay} and {@link #encoderPadding} have been set.
+ */
+ public boolean hasGaplessInfo() {
+ return encoderDelay != Format.NO_VALUE && encoderPadding != Format.NO_VALUE;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
@@ -0,0 +1,210 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * An MPEG audio frame header.
+ */
+public final class MpegAudioHeader {
+
+ /**
+ * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2
+ * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame *
+ * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame.
+ * The next power of two size is 4 KiB.
+ */
+ public static final int MAX_FRAME_SIZE_BYTES = 4096;
+
+ private static final String[] MIME_TYPE_BY_LAYER =
+ new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
+ private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
+ private static final int[] BITRATE_V1_L1 =
+ {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
+ private static final int[] BITRATE_V2_L1 =
+ {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
+ private static final int[] BITRATE_V1_L2 =
+ {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
+ private static final int[] BITRATE_V1_L3 =
+ {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
+ private static final int[] BITRATE_V2 =
+ {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
+
+ /**
+ * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
+ * is invalid.
+ */
+ public static int getFrameSize(int header) {
+ if ((header & 0xFFE00000) != 0xFFE00000) {
+ return C.LENGTH_UNSET;
+ }
+
+ int version = (header >>> 19) & 3;
+ if (version == 1) {
+ return C.LENGTH_UNSET;
+ }
+
+ int layer = (header >>> 17) & 3;
+ if (layer == 0) {
+ return C.LENGTH_UNSET;
+ }
+
+ int bitrateIndex = (header >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return C.LENGTH_UNSET;
+ }
+
+ int samplingRateIndex = (header >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return C.LENGTH_UNSET;
+ }
+
+ int samplingRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ samplingRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ samplingRate /= 4;
+ }
+
+ int bitrate;
+ int padding = (header >>> 9) & 1;
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ return (12000 * bitrate / samplingRate + padding) * 4;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ }
+ }
+
+ if (version == 3) {
+ // Version 1
+ return 144000 * bitrate / samplingRate + padding;
+ } else {
+ // Version 2 or 2.5
+ return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding;
+ }
+ }
+
+ /**
+ * Parses {@code headerData}, populating {@code header} with the parsed data.
+ *
+ * @param headerData Header data to parse.
+ * @param header Header to populate with data from {@code headerData}.
+ * @return True if the header was populated. False otherwise, indicating that {@code headerData}
+ * is not a valid MPEG audio header.
+ */
+ public static boolean populateHeader(int headerData, MpegAudioHeader header) {
+ if ((headerData & 0xFFE00000) != 0xFFE00000) {
+ return false;
+ }
+
+ int version = (headerData >>> 19) & 3;
+ if (version == 1) {
+ return false;
+ }
+
+ int layer = (headerData >>> 17) & 3;
+ if (layer == 0) {
+ return false;
+ }
+
+ int bitrateIndex = (headerData >>> 12) & 15;
+ if (bitrateIndex == 0 || bitrateIndex == 0xF) {
+ // Disallow "free" bitrate.
+ return false;
+ }
+
+ int samplingRateIndex = (headerData >>> 10) & 3;
+ if (samplingRateIndex == 3) {
+ return false;
+ }
+
+ int sampleRate = SAMPLING_RATE_V1[samplingRateIndex];
+ if (version == 2) {
+ // Version 2
+ sampleRate /= 2;
+ } else if (version == 0) {
+ // Version 2.5
+ sampleRate /= 4;
+ }
+
+ int padding = (headerData >>> 9) & 1;
+ int bitrate, frameSize, samplesPerFrame;
+ if (layer == 3) {
+ // Layer I (layer == 3)
+ bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+ frameSize = (12000 * bitrate / sampleRate + padding) * 4;
+ samplesPerFrame = 384;
+ } else {
+ // Layer II (layer == 2) or III (layer == 1)
+ if (version == 3) {
+ // Version 1
+ bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+ samplesPerFrame = 1152;
+ frameSize = 144000 * bitrate / sampleRate + padding;
+ } else {
+ // Version 2 or 2.5.
+ bitrate = BITRATE_V2[bitrateIndex - 1];
+ samplesPerFrame = layer == 1 ? 576 : 1152;
+ frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding;
+ }
+ }
+
+ String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
+ int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
+ header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
+ samplesPerFrame);
+ return true;
+ }
+
+ /** MPEG audio header version. */
+ public int version;
+ /** The mime type. */
+ public String mimeType;
+ /** Size of the frame associated with this header, in bytes. */
+ public int frameSize;
+ /** Sample rate in samples per second. */
+ public int sampleRate;
+ /** Number of audio channels in the frame. */
+ public int channels;
+ /** Bitrate of the frame in bit/s. */
+ public int bitrate;
+ /** Number of samples stored in the frame. */
+ public int samplesPerFrame;
+
+ private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels,
+ int bitrate, int samplesPerFrame) {
+ this.version = version;
+ this.mimeType = mimeType;
+ this.frameSize = frameSize;
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+ this.bitrate = bitrate;
+ this.samplesPerFrame = samplesPerFrame;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/PositionHolder.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+/**
+ * Holds a position in the stream.
+ */
+public final class PositionHolder {
+
+ /**
+ * The held position.
+ */
+ public long position;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/SeekMap.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
+ */
+public interface SeekMap {
+
+ /**
+ * A {@link SeekMap} that does not support seeking.
+ */
+ final class Unseekable implements SeekMap {
+
+ private final long durationUs;
+
+ /**
+ * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
+ * the duration is unknown.
+ */
+ public Unseekable(long durationUs) {
+ this.durationUs = durationUs;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return false;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return 0;
+ }
+
+ }
+
+ /**
+ * Returns whether seeking is supported.
+ * <p>
+ * If seeking is not supported then the only valid seek position is the start of the file, and so
+ * {@link #getPosition(long)} will return 0 for all input values.
+ *
+ * @return Whether seeking is supported.
+ */
+ boolean isSeekable();
+
+ /**
+ * Returns the duration of the stream in microseconds.
+ *
+ * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
+ * duration is unknown.
+ */
+ long getDurationUs();
+
+ /**
+ * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream
+ * from which data can be provided to the extractor.
+ *
+ * @param timeUs A seek position in microseconds.
+ * @return The corresponding position (byte offset) in the stream from which data can be provided
+ * to the extractor, or 0 if {@code #isSeekable()} returns false.
+ */
+ long getPosition(long timeUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -0,0 +1,78 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Receives track level data extracted by an {@link Extractor}.
+ */
+public interface TrackOutput {
+
+ /**
+ * Called when the {@link Format} of the track has been extracted from the stream.
+ *
+ * @param format The extracted {@link Format}.
+ */
+ void format(Format format);
+
+ /**
+ * Called to write sample data to the output.
+ *
+ * @param input An {@link ExtractorInput} from which to read the sample data.
+ * @param length The maximum length to read from the input.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @return The number of bytes appended.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException;
+
+ /**
+ * Called to write sample data to the output.
+ *
+ * @param data A {@link ParsableByteArray} from which to read the sample data.
+ * @param length The number of bytes to read.
+ */
+ void sampleData(ParsableByteArray data, int length);
+
+ /**
+ * Called when metadata associated with a sample has been extracted from the stream.
+ * <p>
+ * The corresponding sample data will have already been passed to the output via calls to
+ * {@link #sampleData(ExtractorInput, int, boolean)} or
+ * {@link #sampleData(ParsableByteArray, int)}.
+ *
+ * @param timeUs The media timestamp associated with the sample, in microseconds.
+ * @param flags Flags associated with the sample. See {@code C.BUFFER_FLAG_*}.
+ * @param size The size of the sample data, in bytes.
+ * @param offset The number of bytes that have been passed to
+ * {@link #sampleData(ExtractorInput, int, boolean)} or
+ * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample
+ * whose metadata is being passed.
+ * @param encryptionKey The encryption key associated with the sample. May be null.
+ */
+ void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ byte[] encryptionKey);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
@@ -0,0 +1,114 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.flv;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+
+/**
+ * Parses audio tags from an FLV stream and extracts AAC frames.
+ */
+/* package */ final class AudioTagPayloadReader extends TagPayloadReader {
+
+ private static final int AUDIO_FORMAT_MP3 = 2;
+ private static final int AUDIO_FORMAT_ALAW = 7;
+ private static final int AUDIO_FORMAT_ULAW = 8;
+ private static final int AUDIO_FORMAT_AAC = 10;
+
+ private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;
+ private static final int AAC_PACKET_TYPE_AAC_RAW = 1;
+
+ private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {5512, 11025, 22050, 44100};
+
+ // State variables
+ private boolean hasParsedAudioDataHeader;
+ private boolean hasOutputFormat;
+ private int audioFormat;
+
+ public AudioTagPayloadReader(TrackOutput output) {
+ super(output);
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
+ if (!hasParsedAudioDataHeader) {
+ int header = data.readUnsignedByte();
+ audioFormat = (header >> 4) & 0x0F;
+ if (audioFormat == AUDIO_FORMAT_MP3) {
+ int sampleRateIndex = (header >> 2) & 0x03;
+ int sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex];
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_MPEG, null,
+ Format.NO_VALUE, Format.NO_VALUE, 1, sampleRate, null, null, 0, null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
+ String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
+ : MimeTypes.AUDIO_ULAW;
+ int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;
+ Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,
+ Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (audioFormat != AUDIO_FORMAT_AAC) {
+ throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
+ }
+ hasParsedAudioDataHeader = true;
+ } else {
+ // Skip header if it was parsed previously.
+ data.skipBytes(1);
+ }
+ return true;
+ }
+
+ @Override
+ protected void parsePayload(ParsableByteArray data, long timeUs) {
+ if (audioFormat == AUDIO_FORMAT_MP3) {
+ int sampleSize = data.bytesLeft();
+ output.sampleData(data, sampleSize);
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ } else {
+ int packetType = data.readUnsignedByte();
+ if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
+ // Parse the sequence header.
+ byte[] audioSpecificConfig = new byte[data.bytesLeft()];
+ data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length);
+ Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
+ audioSpecificConfig);
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
+ Collections.singletonList(audioSpecificConfig), null, 0, null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) {
+ int sampleSize = data.bytesLeft();
+ output.sampleData(data, sampleSize);
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
@@ -0,0 +1,297 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of data from the FLV container format.
+ */
+public final class FlvExtractor implements Extractor, SeekMap {
+
+ /**
+ * Factory for {@link FlvExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new FlvExtractor()};
+ }
+
+ };
+
+ // Header sizes.
+ private static final int FLV_HEADER_SIZE = 9;
+ private static final int FLV_TAG_HEADER_SIZE = 11;
+
+ // Parser states.
+ private static final int STATE_READING_FLV_HEADER = 1;
+ private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;
+ private static final int STATE_READING_TAG_HEADER = 3;
+ private static final int STATE_READING_TAG_DATA = 4;
+
+ // Tag types.
+ private static final int TAG_TYPE_AUDIO = 8;
+ private static final int TAG_TYPE_VIDEO = 9;
+ private static final int TAG_TYPE_SCRIPT_DATA = 18;
+
+ // FLV container identifier.
+ private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
+
+ // Temporary buffers.
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray headerBuffer;
+ private final ParsableByteArray tagHeaderBuffer;
+ private final ParsableByteArray tagData;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+
+ // State variables.
+ private int parserState;
+ private int bytesToNextTagHeader;
+ public int tagType;
+ public int tagDataSize;
+ public long tagTimestampUs;
+
+ // Tags readers.
+ private AudioTagPayloadReader audioReader;
+ private VideoTagPayloadReader videoReader;
+ private ScriptTagPayloadReader metadataReader;
+
+ public FlvExtractor() {
+ scratch = new ParsableByteArray(4);
+ headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
+ tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
+ tagData = new ParsableByteArray();
+ parserState = STATE_READING_FLV_HEADER;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Check if file starts with "FLV" tag
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != FLV_TAG) {
+ return false;
+ }
+
+ // Checking reserved flags are set to 0
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ if ((scratch.readUnsignedShort() & 0xFA) != 0) {
+ return false;
+ }
+
+ // Read data offset
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int dataOffset = scratch.readInt();
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(dataOffset);
+
+ // Checking first "previous tag size" is set to 0
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+
+ return scratch.readInt() == 0;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ parserState = STATE_READING_FLV_HEADER;
+ bytesToNextTagHeader = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_FLV_HEADER:
+ if (!readFlvHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_SKIPPING_TO_TAG_HEADER:
+ skipToTagHeader(input);
+ break;
+ case STATE_READING_TAG_HEADER:
+ if (!readTagHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TAG_DATA:
+ if (readTagData(input)) {
+ return RESULT_CONTINUE;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Reads an FLV container header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if header was read successfully. False if the end of stream was reached.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ headerBuffer.setPosition(0);
+ headerBuffer.skipBytes(4);
+ int flags = headerBuffer.readUnsignedByte();
+ boolean hasAudio = (flags & 0x04) != 0;
+ boolean hasVideo = (flags & 0x01) != 0;
+ if (hasAudio && audioReader == null) {
+ audioReader = new AudioTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
+ }
+ if (hasVideo && videoReader == null) {
+ videoReader = new VideoTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
+ }
+ if (metadataReader == null) {
+ metadataReader = new ScriptTagPayloadReader(null);
+ }
+ extractorOutput.endTracks();
+ extractorOutput.seekMap(this);
+
+ // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.
+ bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
+ parserState = STATE_SKIPPING_TO_TAG_HEADER;
+ return true;
+ }
+
+ /**
+ * Skips over data to reach the next tag header.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @throws IOException If an error occurred skipping data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ input.skipFully(bytesToNextTagHeader);
+ bytesToNextTagHeader = 0;
+ parserState = STATE_READING_TAG_HEADER;
+ }
+
+ /**
+ * Reads a tag header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if tag header was read successfully. Otherwise, false.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ tagHeaderBuffer.setPosition(0);
+ tagType = tagHeaderBuffer.readUnsignedByte();
+ tagDataSize = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
+ tagHeaderBuffer.skipBytes(3); // streamId
+ parserState = STATE_READING_TAG_DATA;
+ return true;
+ }
+
+ /**
+ * Reads the body of a tag from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if the data was consumed by a reader. False if it was skipped.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
+ boolean wasConsumed = true;
+ if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
+ audioReader.consume(prepareTagData(input), tagTimestampUs);
+ } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
+ videoReader.consume(prepareTagData(input), tagTimestampUs);
+ } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
+ metadataReader.consume(prepareTagData(input), tagTimestampUs);
+ } else {
+ input.skipFully(tagDataSize);
+ wasConsumed = false;
+ }
+ bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
+ parserState = STATE_SKIPPING_TO_TAG_HEADER;
+ return wasConsumed;
+ }
+
+ private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException,
+ InterruptedException {
+ if (tagDataSize > tagData.capacity()) {
+ tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0);
+ } else {
+ tagData.setPosition(0);
+ }
+ tagData.setLimit(tagDataSize);
+ input.readFully(tagData.data, 0, tagDataSize);
+ return tagData;
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return false;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return metadataReader.getDurationUs();
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return 0;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
@@ -0,0 +1,218 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parses Script Data tags from an FLV stream and extracts metadata information.
+ */
+/* package */ final class ScriptTagPayloadReader extends TagPayloadReader {
+
+ private static final String NAME_METADATA = "onMetaData";
+ private static final String KEY_DURATION = "duration";
+
+ // AMF object types
+ private static final int AMF_TYPE_NUMBER = 0;
+ private static final int AMF_TYPE_BOOLEAN = 1;
+ private static final int AMF_TYPE_STRING = 2;
+ private static final int AMF_TYPE_OBJECT = 3;
+ private static final int AMF_TYPE_ECMA_ARRAY = 8;
+ private static final int AMF_TYPE_END_MARKER = 9;
+ private static final int AMF_TYPE_STRICT_ARRAY = 10;
+ private static final int AMF_TYPE_DATE = 11;
+
+ private long durationUs;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ public ScriptTagPayloadReader(TrackOutput output) {
+ super(output);
+ durationUs = C.TIME_UNSET;
+ }
+
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) {
+ return true;
+ }
+
+ @Override
+ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ int nameType = readAmfType(data);
+ if (nameType != AMF_TYPE_STRING) {
+ // Should never happen.
+ throw new ParserException();
+ }
+ String name = readAmfString(data);
+ if (!NAME_METADATA.equals(name)) {
+ // We're only interested in metadata.
+ return;
+ }
+ int type = readAmfType(data);
+ if (type != AMF_TYPE_ECMA_ARRAY) {
+ // We're not interested in this metadata.
+ return;
+ }
+ // Set the duration to the value contained in the metadata, if present.
+ Map<String, Object> metadata = readAmfEcmaArray(data);
+ if (metadata.containsKey(KEY_DURATION)) {
+ double durationSeconds = (double) metadata.get(KEY_DURATION);
+ if (durationSeconds > 0.0) {
+ durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
+ }
+ }
+ }
+
+ private static int readAmfType(ParsableByteArray data) {
+ return data.readUnsignedByte();
+ }
+
+ /**
+ * Read a boolean from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Boolean readAmfBoolean(ParsableByteArray data) {
+ return data.readUnsignedByte() == 1;
+ }
+
+ /**
+ * Read a double number from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Double readAmfDouble(ParsableByteArray data) {
+ return Double.longBitsToDouble(data.readLong());
+ }
+
+ /**
+ * Read a string from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static String readAmfString(ParsableByteArray data) {
+ int size = data.readUnsignedShort();
+ int position = data.getPosition();
+ data.skipBytes(size);
+ return new String(data.data, position, size);
+ }
+
+ /**
+ * Read an array from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {
+ int count = data.readUnsignedIntToInt();
+ ArrayList<Object> list = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ int type = readAmfType(data);
+ list.add(readAmfData(data, type));
+ }
+ return list;
+ }
+
+ /**
+ * Read an object from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {
+ HashMap<String, Object> array = new HashMap<>();
+ while (true) {
+ String key = readAmfString(data);
+ int type = readAmfType(data);
+ if (type == AMF_TYPE_END_MARKER) {
+ break;
+ }
+ array.put(key, readAmfData(data, type));
+ }
+ return array;
+ }
+
+ /**
+ * Read an ECMA array from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {
+ int count = data.readUnsignedIntToInt();
+ HashMap<String, Object> array = new HashMap<>(count);
+ for (int i = 0; i < count; i++) {
+ String key = readAmfString(data);
+ int type = readAmfType(data);
+ array.put(key, readAmfData(data, type));
+ }
+ return array;
+ }
+
+ /**
+ * Read a date from an AMF encoded buffer.
+ *
+ * @param data The buffer from which to read.
+ * @return The value read from the buffer.
+ */
+ private static Date readAmfDate(ParsableByteArray data) {
+ Date date = new Date((long) readAmfDouble(data).doubleValue());
+ data.skipBytes(2); // Skip reserved bytes.
+ return date;
+ }
+
+ private static Object readAmfData(ParsableByteArray data, int type) {
+ switch (type) {
+ case AMF_TYPE_NUMBER:
+ return readAmfDouble(data);
+ case AMF_TYPE_BOOLEAN:
+ return readAmfBoolean(data);
+ case AMF_TYPE_STRING:
+ return readAmfString(data);
+ case AMF_TYPE_OBJECT:
+ return readAmfObject(data);
+ case AMF_TYPE_ECMA_ARRAY:
+ return readAmfEcmaArray(data);
+ case AMF_TYPE_STRICT_ARRAY:
+ return readAmfStrictArray(data);
+ case AMF_TYPE_DATE:
+ return readAmfDate(data);
+ default:
+ return null;
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Extracts individual samples from FLV tags, preserving original order.
+ */
+/* package */ abstract class TagPayloadReader {
+
+ /**
+ * Thrown when the format is not supported.
+ */
+ public static final class UnsupportedFormatException extends ParserException {
+
+ public UnsupportedFormatException(String msg) {
+ super(msg);
+ }
+
+ }
+
+ protected final TrackOutput output;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ protected TagPayloadReader(TrackOutput output) {
+ this.output = output;
+ }
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that
+ * was previously passed. Hence the reader should reset any internal state.
+ */
+ public abstract void seek();
+
+ /**
+ * Consumes payload data.
+ *
+ * @param data The payload data to consume.
+ * @param timeUs The timestamp associated with the payload.
+ * @throws ParserException If an error occurs parsing the data.
+ */
+ public final void consume(ParsableByteArray data, long timeUs) throws ParserException {
+ if (parseHeader(data)) {
+ parsePayload(data, timeUs);
+ }
+ }
+
+ /**
+ * Parses tag header.
+ *
+ * @param data Buffer where the tag header is stored.
+ * @return Whether the header was parsed successfully.
+ * @throws ParserException If an error occurs parsing the header.
+ */
+ protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException;
+
+ /**
+ * Parses tag payload.
+ *
+ * @param data Buffer where tag payload is stored
+ * @param timeUs Time position of the frame
+ * @throws ParserException If an error occurs parsing the payload.
+ */
+ protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
@@ -0,0 +1,130 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.video.AvcConfig;
+
+/**
+ * Parses video tags from an FLV stream and extracts H.264 nal units.
+ */
+/* package */ final class VideoTagPayloadReader extends TagPayloadReader {
+
+ // Video codec.
+ private static final int VIDEO_CODEC_AVC = 7;
+
+ // Frame types.
+ private static final int VIDEO_FRAME_KEYFRAME = 1;
+ private static final int VIDEO_FRAME_VIDEO_INFO = 5;
+
+ // Packet types.
+ private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0;
+ private static final int AVC_PACKET_TYPE_AVC_NALU = 1;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private int nalUnitLengthFieldLength;
+
+ // State variables.
+ private boolean hasOutputFormat;
+ private int frameType;
+
+ /**
+ * @param output A {@link TrackOutput} to which samples should be written.
+ */
+ public VideoTagPayloadReader(TrackOutput output) {
+ super(output);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ }
+
+ @Override
+ public void seek() {
+ // Do nothing.
+ }
+
+ @Override
+ protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
+ int header = data.readUnsignedByte();
+ int frameType = (header >> 4) & 0x0F;
+ int videoCodec = (header & 0x0F);
+ // Support just H.264 encoded content.
+ if (videoCodec != VIDEO_CODEC_AVC) {
+ throw new UnsupportedFormatException("Video format not supported: " + videoCodec);
+ }
+ this.frameType = frameType;
+ return (frameType != VIDEO_FRAME_VIDEO_INFO);
+ }
+
+ @Override
+ protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+ int packetType = data.readUnsignedByte();
+ int compositionTimeMs = data.readUnsignedInt24();
+ timeUs += compositionTimeMs * 1000L;
+ // Parse avc sequence header in case this was not done before.
+ if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
+ ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]);
+ data.readBytes(videoSequence.data, 0, data.bytesLeft());
+ AvcConfig avcConfig = AvcConfig.parse(videoSequence);
+ nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ // Construct and output the format.
+ Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,
+ Format.NO_VALUE, Format.NO_VALUE, avcConfig.width, avcConfig.height, Format.NO_VALUE,
+ avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null);
+ output.format(format);
+ hasOutputFormat = true;
+ } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) {
+ // TODO: Deduplicate with Mp4Extractor.
+ // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
+ // they're only 1 or 2 bytes long.
+ byte[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ int bytesWritten = 0;
+ int bytesToWrite;
+ while (data.bytesLeft() > 0) {
+ // Read the NAL length so that we know where we find the next one.
+ data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ nalLength.setPosition(0);
+ bytesToWrite = nalLength.readUnsignedIntToInt();
+
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ bytesWritten += 4;
+
+ // Write the payload of the NAL unit.
+ output.sampleData(data, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0,
+ bytesWritten, 0, null);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
@@ -0,0 +1,239 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Stack;
+
+/**
+ * Default implementation of {@link EbmlReader}.
+ */
+/* package */ final class DefaultEbmlReader implements EbmlReader {
+
+ private static final int ELEMENT_STATE_READ_ID = 0;
+ private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;
+ private static final int ELEMENT_STATE_READ_CONTENT = 2;
+
+ private static final int MAX_ID_BYTES = 4;
+ private static final int MAX_LENGTH_BYTES = 8;
+
+ private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
+ private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
+ private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
+
+ private final byte[] scratch = new byte[8];
+ private final Stack<MasterElement> masterElementsStack = new Stack<>();
+ private final VarintReader varintReader = new VarintReader();
+
+ private EbmlReaderOutput output;
+ private int elementState;
+ private int elementId;
+ private long elementContentSize;
+
+ @Override
+ public void init(EbmlReaderOutput eventHandler) {
+ this.output = eventHandler;
+ }
+
+ @Override
+ public void reset() {
+ elementState = ELEMENT_STATE_READ_ID;
+ masterElementsStack.clear();
+ varintReader.reset();
+ }
+
+ @Override
+ public boolean read(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkState(output != null);
+ while (true) {
+ if (!masterElementsStack.isEmpty()
+ && input.getPosition() >= masterElementsStack.peek().elementEndPosition) {
+ output.endMasterElement(masterElementsStack.pop().elementId);
+ return true;
+ }
+
+ if (elementState == ELEMENT_STATE_READ_ID) {
+ long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES);
+ if (result == C.RESULT_MAX_LENGTH_EXCEEDED) {
+ result = maybeResyncToNextLevel1Element(input);
+ }
+ if (result == C.RESULT_END_OF_INPUT) {
+ return false;
+ }
+ // Element IDs are at most 4 bytes, so we can cast to integers.
+ elementId = (int) result;
+ elementState = ELEMENT_STATE_READ_CONTENT_SIZE;
+ }
+
+ if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {
+ elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES);
+ elementState = ELEMENT_STATE_READ_CONTENT;
+ }
+
+ int type = output.getElementType(elementId);
+ switch (type) {
+ case TYPE_MASTER:
+ long elementContentPosition = input.getPosition();
+ long elementEndPosition = elementContentPosition + elementContentSize;
+ masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
+ output.startMasterElement(elementId, elementContentPosition, elementContentSize);
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case TYPE_UNSIGNED_INT:
+ if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
+ throw new ParserException("Invalid integer size: " + elementContentSize);
+ }
+ output.integerElement(elementId, readInteger(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case TYPE_FLOAT:
+ if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
+ && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
+ throw new ParserException("Invalid float size: " + elementContentSize);
+ }
+ output.floatElement(elementId, readFloat(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case TYPE_STRING:
+ if (elementContentSize > Integer.MAX_VALUE) {
+ throw new ParserException("String element size: " + elementContentSize);
+ }
+ output.stringElement(elementId, readString(input, (int) elementContentSize));
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case TYPE_BINARY:
+ output.binaryElement(elementId, (int) elementContentSize, input);
+ elementState = ELEMENT_STATE_READ_ID;
+ return true;
+ case TYPE_UNKNOWN:
+ input.skipFully((int) elementContentSize);
+ elementState = ELEMENT_STATE_READ_ID;
+ break;
+ default:
+ throw new ParserException("Invalid element type " + type);
+ }
+ }
+ }
+
+ /**
+ * Does a byte by byte search to try and find the next level 1 element. This method is called if
+ * some invalid data is encountered in the parser.
+ *
+ * @param input The {@link ExtractorInput} from which data has to be read.
+ * @return id of the next level 1 element that has been found.
+ * @throws EOFException If the end of input was encountered when searching for the next level 1
+ * element.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException,
+ InterruptedException {
+ input.resetPeekPosition();
+ while (true) {
+ input.peekFully(scratch, 0, MAX_ID_BYTES);
+ int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]);
+ if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) {
+ int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false);
+ if (output.isLevel1Element(potentialId)) {
+ input.skipFully(varintLength);
+ return potentialId;
+ }
+ }
+ input.skipFully(1);
+ }
+ }
+
+ /**
+ * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the integer being read.
+ * @return The read integer value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private long readInteger(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ input.readFully(scratch, 0, byteLength);
+ long value = 0;
+ for (int i = 0; i < byteLength; i++) {
+ value = (value << 8) | (scratch[i] & 0xFF);
+ }
+ return value;
+ }
+
+ /**
+ * Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the float being read.
+ * @return The read float value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private double readFloat(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ long integerValue = readInteger(input, byteLength);
+ double floatValue;
+ if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
+ floatValue = Float.intBitsToFloat((int) integerValue);
+ } else {
+ floatValue = Double.longBitsToDouble(integerValue);
+ }
+ return floatValue;
+ }
+
+ /**
+ * Reads and returns a string of length {@code byteLength} from the {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @param byteLength The length of the float being read.
+ * @return The read string value.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private String readString(ExtractorInput input, int byteLength)
+ throws IOException, InterruptedException {
+ if (byteLength == 0) {
+ return "";
+ }
+ byte[] stringBytes = new byte[byteLength];
+ input.readFully(stringBytes, 0, byteLength);
+ return new String(stringBytes);
+ }
+
+ /**
+ * Used in {@link #masterElementsStack} to track when the current master element ends, so that
+ * {@link EbmlReaderOutput#endMasterElement(int)} can be called.
+ */
+ private static final class MasterElement {
+
+ private final int elementId;
+ private final long elementEndPosition;
+
+ private MasterElement(int elementId, long elementEndPosition) {
+ this.elementId = elementId;
+ this.elementEndPosition = elementEndPosition;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+
+/**
+ * Event-driven EBML reader that delivers events to an {@link EbmlReaderOutput}.
+ * <p>
+ * EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was
+ * originally designed for the Matroska container format. More information about EBML and
+ * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
+ */
+/* package */ interface EbmlReader {
+
+ /**
+ * Type for unknown elements.
+ */
+ int TYPE_UNKNOWN = 0;
+ /**
+ * Type for elements that contain child elements.
+ */
+ int TYPE_MASTER = 1;
+ /**
+ * Type for integer value elements of up to 8 bytes.
+ */
+ int TYPE_UNSIGNED_INT = 2;
+ /**
+ * Type for string elements.
+ */
+ int TYPE_STRING = 3;
+ /**
+ * Type for binary elements.
+ */
+ int TYPE_BINARY = 4;
+ /**
+ * Type for IEEE floating point value elements of either 4 or 8 bytes.
+ */
+ int TYPE_FLOAT = 5;
+
+ /**
+ * Initializes the extractor with an {@link EbmlReaderOutput}.
+ *
+ * @param output An {@link EbmlReaderOutput} to receive events.
+ */
+ void init(EbmlReaderOutput output);
+
+ /**
+ * Resets the state of the reader.
+ * <p>
+ * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure
+ * from scratch.
+ */
+ void reset();
+
+ /**
+ * Reads from an {@link ExtractorInput}, invoking an event callback if possible.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @return True if data can continue to be read. False if the end of the input was encountered.
+ * @throws ParserException If parsing fails.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ boolean read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java
@@ -0,0 +1,116 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+
+/**
+ * Defines EBML element IDs/types and reacts to events.
+ */
+/* package */ interface EbmlReaderOutput {
+
+ /**
+ * Maps an element ID to a corresponding type.
+ * <p>
+ * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all
+ * children of a skipped element are also skipped.
+ *
+ * @param id The element ID to map.
+ * @return One of the {@code TYPE_} constants defined in {@link EbmlReader}.
+ */
+ int getElementType(int id);
+
+ /**
+ * Checks if the given id is that of a level 1 element.
+ *
+ * @param id The element ID.
+ * @return Whether the given id is that of a level 1 element.
+ */
+ boolean isLevel1Element(int id);
+
+ /**
+ * Called when the start of a master element is encountered.
+ * <p>
+ * Following events should be considered as taking place within this element until a matching call
+ * to {@link #endMasterElement(int)} is made.
+ * <p>
+ * Note that it is possible for another master element of the same element ID to be nested within
+ * itself.
+ *
+ * @param id The element ID.
+ * @param contentPosition The position of the start of the element's content in the stream.
+ * @param contentSize The size of the element's content in bytes.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException;
+
+ /**
+ * Called when the end of a master element is encountered.
+ *
+ * @param id The element ID.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void endMasterElement(int id) throws ParserException;
+
+ /**
+ * Called when an integer element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The integer value that the element contains.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void integerElement(int id, long value) throws ParserException;
+
+ /**
+ * Called when a float element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The float value that the element contains
+ * @throws ParserException If a parsing error occurs.
+ */
+ void floatElement(int id, double value) throws ParserException;
+
+ /**
+ * Called when a string element is encountered.
+ *
+ * @param id The element ID.
+ * @param value The string value that the element contains.
+ * @throws ParserException If a parsing error occurs.
+ */
+ void stringElement(int id, String value) throws ParserException;
+
+ /**
+ * Called when a binary element is encountered.
+ * <p>
+ * The element header (containing the element ID and content size) will already have been read.
+ * Implementations are required to consume the whole remainder of the element, which is
+ * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail
+ * (by throwing an exception) having partially consumed the data, however if they do this, they
+ * must consume the remainder of the content when called again.
+ *
+ * @param id The element ID.
+ * @param contentsSize The element's content size.
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @throws ParserException If a parsing error occurs.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ void binaryElement(int id, int contentsSize, ExtractorInput input)
+ throws IOException, InterruptedException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -0,0 +1,1846 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mkv;
+
+import android.support.annotation.IntDef;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.LongArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.AvcConfig;
+import com.google.android.exoplayer2.video.ColorInfo;
+import com.google.android.exoplayer2.video.HevcConfig;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * Extracts data from a Matroska or WebM file.
+ */
+public final class MatroskaExtractor implements Extractor {
+
+ /**
+ * Factory for {@link MatroskaExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new MatroskaExtractor()};
+ }
+
+ };
+
+ /**
+ * Flags controlling the behavior of the extractor.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_DISABLE_SEEK_FOR_CUES})
+ public @interface Flags {}
+ /**
+ * Flag to disable seeking for cues.
+ * <p>
+ * Normally (i.e. when this flag is not set) the extractor will seek to the cues element if its
+ * position is specified in the seek head and if it's after the first cluster. Setting this flag
+ * disables seeking to the cues element. If the cues element is after the first cluster then the
+ * media is treated as being unseekable.
+ */
+ public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1;
+
+ private static final int UNSET_ENTRY_ID = -1;
+
+ private static final int BLOCK_STATE_START = 0;
+ private static final int BLOCK_STATE_HEADER = 1;
+ private static final int BLOCK_STATE_DATA = 2;
+
+ private static final String DOC_TYPE_MATROSKA = "matroska";
+ private static final String DOC_TYPE_WEBM = "webm";
+ private static final String CODEC_ID_VP8 = "V_VP8";
+ private static final String CODEC_ID_VP9 = "V_VP9";
+ private static final String CODEC_ID_MPEG2 = "V_MPEG2";
+ private static final String CODEC_ID_MPEG4_SP = "V_MPEG4/ISO/SP";
+ private static final String CODEC_ID_MPEG4_ASP = "V_MPEG4/ISO/ASP";
+ private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP";
+ private static final String CODEC_ID_H264 = "V_MPEG4/ISO/AVC";
+ private static final String CODEC_ID_H265 = "V_MPEGH/ISO/HEVC";
+ private static final String CODEC_ID_FOURCC = "V_MS/VFW/FOURCC";
+ private static final String CODEC_ID_THEORA = "V_THEORA";
+ private static final String CODEC_ID_VORBIS = "A_VORBIS";
+ private static final String CODEC_ID_OPUS = "A_OPUS";
+ private static final String CODEC_ID_AAC = "A_AAC";
+ private static final String CODEC_ID_MP2 = "A_MPEG/L2";
+ private static final String CODEC_ID_MP3 = "A_MPEG/L3";
+ private static final String CODEC_ID_AC3 = "A_AC3";
+ private static final String CODEC_ID_E_AC3 = "A_EAC3";
+ private static final String CODEC_ID_TRUEHD = "A_TRUEHD";
+ private static final String CODEC_ID_DTS = "A_DTS";
+ private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS";
+ private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS";
+ private static final String CODEC_ID_FLAC = "A_FLAC";
+ private static final String CODEC_ID_ACM = "A_MS/ACM";
+ private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
+ private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
+ private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
+ private static final String CODEC_ID_PGS = "S_HDMV/PGS";
+ private static final String CODEC_ID_DVBSUB = "S_DVBSUB";
+
+ private static final int VORBIS_MAX_INPUT_SIZE = 8192;
+ private static final int OPUS_MAX_INPUT_SIZE = 5760;
+ private static final int ENCRYPTION_IV_SIZE = 8;
+ private static final int TRACK_TYPE_AUDIO = 2;
+
+ private static final int ID_EBML = 0x1A45DFA3;
+ private static final int ID_EBML_READ_VERSION = 0x42F7;
+ private static final int ID_DOC_TYPE = 0x4282;
+ private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
+ private static final int ID_SEGMENT = 0x18538067;
+ private static final int ID_SEGMENT_INFO = 0x1549A966;
+ private static final int ID_SEEK_HEAD = 0x114D9B74;
+ private static final int ID_SEEK = 0x4DBB;
+ private static final int ID_SEEK_ID = 0x53AB;
+ private static final int ID_SEEK_POSITION = 0x53AC;
+ private static final int ID_INFO = 0x1549A966;
+ private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
+ private static final int ID_DURATION = 0x4489;
+ private static final int ID_CLUSTER = 0x1F43B675;
+ private static final int ID_TIME_CODE = 0xE7;
+ private static final int ID_SIMPLE_BLOCK = 0xA3;
+ private static final int ID_BLOCK_GROUP = 0xA0;
+ private static final int ID_BLOCK = 0xA1;
+ private static final int ID_BLOCK_DURATION = 0x9B;
+ private static final int ID_REFERENCE_BLOCK = 0xFB;
+ private static final int ID_TRACKS = 0x1654AE6B;
+ private static final int ID_TRACK_ENTRY = 0xAE;
+ private static final int ID_TRACK_NUMBER = 0xD7;
+ private static final int ID_TRACK_TYPE = 0x83;
+ private static final int ID_FLAG_DEFAULT = 0x88;
+ private static final int ID_FLAG_FORCED = 0x55AA;
+ private static final int ID_DEFAULT_DURATION = 0x23E383;
+ private static final int ID_CODEC_ID = 0x86;
+ private static final int ID_CODEC_PRIVATE = 0x63A2;
+ private static final int ID_CODEC_DELAY = 0x56AA;
+ private static final int ID_SEEK_PRE_ROLL = 0x56BB;
+ private static final int ID_VIDEO = 0xE0;
+ private static final int ID_PIXEL_WIDTH = 0xB0;
+ private static final int ID_PIXEL_HEIGHT = 0xBA;
+ private static final int ID_DISPLAY_WIDTH = 0x54B0;
+ private static final int ID_DISPLAY_HEIGHT = 0x54BA;
+ private static final int ID_DISPLAY_UNIT = 0x54B2;
+ private static final int ID_AUDIO = 0xE1;
+ private static final int ID_CHANNELS = 0x9F;
+ private static final int ID_AUDIO_BIT_DEPTH = 0x6264;
+ private static final int ID_SAMPLING_FREQUENCY = 0xB5;
+ private static final int ID_CONTENT_ENCODINGS = 0x6D80;
+ private static final int ID_CONTENT_ENCODING = 0x6240;
+ private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
+ private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
+ private static final int ID_CONTENT_COMPRESSION = 0x5034;
+ private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254;
+ private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255;
+ private static final int ID_CONTENT_ENCRYPTION = 0x5035;
+ private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
+ private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
+ private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;
+ private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;
+ private static final int ID_CUES = 0x1C53BB6B;
+ private static final int ID_CUE_POINT = 0xBB;
+ private static final int ID_CUE_TIME = 0xB3;
+ private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
+ private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
+ private static final int ID_LANGUAGE = 0x22B59C;
+ private static final int ID_PROJECTION = 0x7670;
+ private static final int ID_PROJECTION_PRIVATE = 0x7672;
+ private static final int ID_STEREO_MODE = 0x53B8;
+ private static final int ID_COLOUR = 0x55B0;
+ private static final int ID_COLOUR_RANGE = 0x55B9;
+ private static final int ID_COLOUR_TRANSFER = 0x55BA;
+ private static final int ID_COLOUR_PRIMARIES = 0x55BB;
+ private static final int ID_MAX_CLL = 0x55BC;
+ private static final int ID_MAX_FALL = 0x55BD;
+ private static final int ID_MASTERING_METADATA = 0x55D0;
+ private static final int ID_PRIMARY_R_CHROMATICITY_X = 0x55D1;
+ private static final int ID_PRIMARY_R_CHROMATICITY_Y = 0x55D2;
+ private static final int ID_PRIMARY_G_CHROMATICITY_X = 0x55D3;
+ private static final int ID_PRIMARY_G_CHROMATICITY_Y = 0x55D4;
+ private static final int ID_PRIMARY_B_CHROMATICITY_X = 0x55D5;
+ private static final int ID_PRIMARY_B_CHROMATICITY_Y = 0x55D6;
+ private static final int ID_WHITE_POINT_CHROMATICITY_X = 0x55D7;
+ private static final int ID_WHITE_POINT_CHROMATICITY_Y = 0x55D8;
+ private static final int ID_LUMNINANCE_MAX = 0x55D9;
+ private static final int ID_LUMNINANCE_MIN = 0x55DA;
+
+ private static final int LACING_NONE = 0;
+ private static final int LACING_XIPH = 1;
+ private static final int LACING_FIXED_SIZE = 2;
+ private static final int LACING_EBML = 3;
+
+ private static final int FOURCC_COMPRESSION_VC1 = 0x31435657;
+
+ /**
+ * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode
+ * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be
+ * replaced with the duration of the subtitle.
+ * <p>
+ * Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n".
+ */
+ private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48,
+ 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10};
+ /**
+ * A special end timecode indicating that a subtitle should be displayed until the next subtitle,
+ * or until the end of the media in the case of the last subtitle.
+ * <p>
+ * Equivalent to the UTF-8 string: " ".
+ */
+ private static final byte[] SUBRIP_TIMECODE_EMPTY =
+ new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32};
+ /**
+ * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
+ */
+ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
+ /**
+ * The length in bytes of a timecode in a subrip prefix.
+ */
+ private static final int SUBRIP_TIMECODE_LENGTH = 12;
+
+ /**
+ * The length in bytes of a WAVEFORMATEX structure.
+ */
+ private static final int WAVE_FORMAT_SIZE = 18;
+ /**
+ * Format tag indicating a WAVEFORMATEXTENSIBLE structure.
+ */
+ private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+ /**
+ * Format tag for PCM.
+ */
+ private static final int WAVE_FORMAT_PCM = 1;
+ /**
+ * Sub format for PCM.
+ */
+ private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L);
+
+ private final EbmlReader reader;
+ private final VarintReader varintReader;
+ private final SparseArray<Track> tracks;
+ private final boolean seekForCuesEnabled;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray vorbisNumPageSamples;
+ private final ParsableByteArray seekEntryIdBytes;
+ private final ParsableByteArray sampleStrippedBytes;
+ private final ParsableByteArray subripSample;
+ private final ParsableByteArray encryptionInitializationVector;
+ private final ParsableByteArray encryptionSubsampleData;
+ private ByteBuffer encryptionSubsampleDataBuffer;
+
+ private long segmentContentSize;
+ private long segmentContentPosition = C.POSITION_UNSET;
+ private long timecodeScale = C.TIME_UNSET;
+ private long durationTimecode = C.TIME_UNSET;
+ private long durationUs = C.TIME_UNSET;
+
+ // The track corresponding to the current TrackEntry element, or null.
+ private Track currentTrack;
+
+ // Whether a seek map has been sent to the output.
+ private boolean sentSeekMap;
+
+ // Master seek entry related elements.
+ private int seekEntryId;
+ private long seekEntryPosition;
+
+ // Cue related elements.
+ private boolean seekForCues;
+ private long cuesContentPosition = C.POSITION_UNSET;
+ private long seekPositionAfterBuildingCues = C.POSITION_UNSET;
+ private long clusterTimecodeUs = C.TIME_UNSET;
+ private LongArray cueTimesUs;
+ private LongArray cueClusterPositions;
+ private boolean seenClusterPositionForCurrentCuePoint;
+
+ // Block reading state.
+ private int blockState;
+ private long blockTimeUs;
+ private long blockDurationUs;
+ private int blockLacingSampleIndex;
+ private int blockLacingSampleCount;
+ private int[] blockLacingSampleSizes;
+ private int blockTrackNumber;
+ private int blockTrackNumberLength;
+ @C.BufferFlags
+ private int blockFlags;
+
+ // Sample reading state.
+ private int sampleBytesRead;
+ private boolean sampleEncodingHandled;
+ private boolean sampleSignalByteRead;
+ private boolean sampleInitializationVectorRead;
+ private boolean samplePartitionCountRead;
+ private byte sampleSignalByte;
+ private int samplePartitionCount;
+ private int sampleCurrentNalBytesRemaining;
+ private int sampleBytesWritten;
+ private boolean sampleRead;
+ private boolean sampleSeenReferenceBlock;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+
+ public MatroskaExtractor() {
+ this(0);
+ }
+
+ public MatroskaExtractor(@Flags int flags) {
+ this(new DefaultEbmlReader(), flags);
+ }
+
+ /* package */ MatroskaExtractor(EbmlReader reader, @Flags int flags) {
+ this.reader = reader;
+ this.reader.init(new InnerEbmlReaderOutput());
+ seekForCuesEnabled = (flags & FLAG_DISABLE_SEEK_FOR_CUES) == 0;
+ varintReader = new VarintReader();
+ tracks = new SparseArray<>();
+ scratch = new ParsableByteArray(4);
+ vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array());
+ seekEntryIdBytes = new ParsableByteArray(4);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ sampleStrippedBytes = new ParsableByteArray();
+ subripSample = new ParsableByteArray();
+ encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);
+ encryptionSubsampleData = new ParsableByteArray();
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return new Sniffer().sniff(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ clusterTimecodeUs = C.TIME_UNSET;
+ blockState = BLOCK_STATE_START;
+ reader.reset();
+ varintReader.reset();
+ resetSample();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ sampleRead = false;
+ boolean continueReading = true;
+ while (continueReading && !sampleRead) {
+ continueReading = reader.read(input);
+ if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) {
+ return Extractor.RESULT_SEEK;
+ }
+ }
+ return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT;
+ }
+
+ /* package */ int getElementType(int id) {
+ switch (id) {
+ case ID_EBML:
+ case ID_SEGMENT:
+ case ID_SEEK_HEAD:
+ case ID_SEEK:
+ case ID_INFO:
+ case ID_CLUSTER:
+ case ID_TRACKS:
+ case ID_TRACK_ENTRY:
+ case ID_AUDIO:
+ case ID_VIDEO:
+ case ID_CONTENT_ENCODINGS:
+ case ID_CONTENT_ENCODING:
+ case ID_CONTENT_COMPRESSION:
+ case ID_CONTENT_ENCRYPTION:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
+ case ID_CUES:
+ case ID_CUE_POINT:
+ case ID_CUE_TRACK_POSITIONS:
+ case ID_BLOCK_GROUP:
+ case ID_PROJECTION:
+ case ID_COLOUR:
+ case ID_MASTERING_METADATA:
+ return EbmlReader.TYPE_MASTER;
+ case ID_EBML_READ_VERSION:
+ case ID_DOC_TYPE_READ_VERSION:
+ case ID_SEEK_POSITION:
+ case ID_TIMECODE_SCALE:
+ case ID_TIME_CODE:
+ case ID_BLOCK_DURATION:
+ case ID_PIXEL_WIDTH:
+ case ID_PIXEL_HEIGHT:
+ case ID_DISPLAY_WIDTH:
+ case ID_DISPLAY_HEIGHT:
+ case ID_DISPLAY_UNIT:
+ case ID_TRACK_NUMBER:
+ case ID_TRACK_TYPE:
+ case ID_FLAG_DEFAULT:
+ case ID_FLAG_FORCED:
+ case ID_DEFAULT_DURATION:
+ case ID_CODEC_DELAY:
+ case ID_SEEK_PRE_ROLL:
+ case ID_CHANNELS:
+ case ID_AUDIO_BIT_DEPTH:
+ case ID_CONTENT_ENCODING_ORDER:
+ case ID_CONTENT_ENCODING_SCOPE:
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ case ID_CUE_TIME:
+ case ID_CUE_CLUSTER_POSITION:
+ case ID_REFERENCE_BLOCK:
+ case ID_STEREO_MODE:
+ case ID_COLOUR_RANGE:
+ case ID_COLOUR_TRANSFER:
+ case ID_COLOUR_PRIMARIES:
+ case ID_MAX_CLL:
+ case ID_MAX_FALL:
+ return EbmlReader.TYPE_UNSIGNED_INT;
+ case ID_DOC_TYPE:
+ case ID_CODEC_ID:
+ case ID_LANGUAGE:
+ return EbmlReader.TYPE_STRING;
+ case ID_SEEK_ID:
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ case ID_SIMPLE_BLOCK:
+ case ID_BLOCK:
+ case ID_CODEC_PRIVATE:
+ case ID_PROJECTION_PRIVATE:
+ return EbmlReader.TYPE_BINARY;
+ case ID_DURATION:
+ case ID_SAMPLING_FREQUENCY:
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ case ID_LUMNINANCE_MAX:
+ case ID_LUMNINANCE_MIN:
+ return EbmlReader.TYPE_FLOAT;
+ default:
+ return EbmlReader.TYPE_UNKNOWN;
+ }
+ }
+
+ /* package */ boolean isLevel1Element(int id) {
+ return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS;
+ }
+
+ /* package */ void startMasterElement(int id, long contentPosition, long contentSize)
+ throws ParserException {
+ switch (id) {
+ case ID_SEGMENT:
+ if (segmentContentPosition != C.POSITION_UNSET
+ && segmentContentPosition != contentPosition) {
+ throw new ParserException("Multiple Segment elements not supported");
+ }
+ segmentContentPosition = contentPosition;
+ segmentContentSize = contentSize;
+ break;
+ case ID_SEEK:
+ seekEntryId = UNSET_ENTRY_ID;
+ seekEntryPosition = C.POSITION_UNSET;
+ break;
+ case ID_CUES:
+ cueTimesUs = new LongArray();
+ cueClusterPositions = new LongArray();
+ break;
+ case ID_CUE_POINT:
+ seenClusterPositionForCurrentCuePoint = false;
+ break;
+ case ID_CLUSTER:
+ if (!sentSeekMap) {
+ // We need to build cues before parsing the cluster.
+ if (seekForCuesEnabled && cuesContentPosition != C.POSITION_UNSET) {
+ // We know where the Cues element is located. Seek to request it.
+ seekForCues = true;
+ } else {
+ // We don't know where the Cues element is located. It's most likely omitted. Allow
+ // playback, but disable seeking.
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+ sentSeekMap = true;
+ }
+ }
+ break;
+ case ID_BLOCK_GROUP:
+ sampleSeenReferenceBlock = false;
+ break;
+ case ID_CONTENT_ENCODING:
+ // TODO: check and fail if more than one content encoding is present.
+ break;
+ case ID_CONTENT_ENCRYPTION:
+ currentTrack.hasContentEncryption = true;
+ break;
+ case ID_TRACK_ENTRY:
+ currentTrack = new Track();
+ break;
+ case ID_MASTERING_METADATA:
+ currentTrack.hasColorInfo = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* package */ void endMasterElement(int id) throws ParserException {
+ switch (id) {
+ case ID_SEGMENT_INFO:
+ if (timecodeScale == C.TIME_UNSET) {
+ // timecodeScale was omitted. Use the default value.
+ timecodeScale = 1000000;
+ }
+ if (durationTimecode != C.TIME_UNSET) {
+ durationUs = scaleTimecodeToUs(durationTimecode);
+ }
+ break;
+ case ID_SEEK:
+ if (seekEntryId == UNSET_ENTRY_ID || seekEntryPosition == C.POSITION_UNSET) {
+ throw new ParserException("Mandatory element SeekID or SeekPosition not found");
+ }
+ if (seekEntryId == ID_CUES) {
+ cuesContentPosition = seekEntryPosition;
+ }
+ break;
+ case ID_CUES:
+ if (!sentSeekMap) {
+ extractorOutput.seekMap(buildSeekMap());
+ sentSeekMap = true;
+ } else {
+ // We have already built the cues. Ignore.
+ }
+ break;
+ case ID_BLOCK_GROUP:
+ if (blockState != BLOCK_STATE_DATA) {
+ // We've skipped this block (due to incompatible track number).
+ return;
+ }
+ // If the ReferenceBlock element was not found for this sample, then it is a keyframe.
+ if (!sampleSeenReferenceBlock) {
+ blockFlags |= C.BUFFER_FLAG_KEY_FRAME;
+ }
+ commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs);
+ blockState = BLOCK_STATE_START;
+ break;
+ case ID_CONTENT_ENCODING:
+ if (currentTrack.hasContentEncryption) {
+ if (currentTrack.encryptionKeyId == null) {
+ throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
+ }
+ currentTrack.drmInitData = new DrmInitData(
+ new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId));
+ }
+ break;
+ case ID_CONTENT_ENCODINGS:
+ if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) {
+ throw new ParserException("Combining encryption and compression is not supported");
+ }
+ break;
+ case ID_TRACK_ENTRY:
+ if (isCodecSupported(currentTrack.codecId)) {
+ currentTrack.initializeOutput(extractorOutput, currentTrack.number);
+ tracks.put(currentTrack.number, currentTrack);
+ }
+ currentTrack = null;
+ break;
+ case ID_TRACKS:
+ if (tracks.size() == 0) {
+ throw new ParserException("No valid tracks were found");
+ }
+ extractorOutput.endTracks();
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* package */ void integerElement(int id, long value) throws ParserException {
+ switch (id) {
+ case ID_EBML_READ_VERSION:
+ // Validate that EBMLReadVersion is supported. This extractor only supports v1.
+ if (value != 1) {
+ throw new ParserException("EBMLReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_DOC_TYPE_READ_VERSION:
+ // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
+ if (value < 1 || value > 2) {
+ throw new ParserException("DocTypeReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_SEEK_POSITION:
+ // Seek Position is the relative offset beginning from the Segment. So to get absolute
+ // offset from the beginning of the file, we need to add segmentContentPosition to it.
+ seekEntryPosition = value + segmentContentPosition;
+ break;
+ case ID_TIMECODE_SCALE:
+ timecodeScale = value;
+ break;
+ case ID_PIXEL_WIDTH:
+ currentTrack.width = (int) value;
+ break;
+ case ID_PIXEL_HEIGHT:
+ currentTrack.height = (int) value;
+ break;
+ case ID_DISPLAY_WIDTH:
+ currentTrack.displayWidth = (int) value;
+ break;
+ case ID_DISPLAY_HEIGHT:
+ currentTrack.displayHeight = (int) value;
+ break;
+ case ID_DISPLAY_UNIT:
+ currentTrack.displayUnit = (int) value;
+ break;
+ case ID_TRACK_NUMBER:
+ currentTrack.number = (int) value;
+ break;
+ case ID_FLAG_DEFAULT:
+ currentTrack.flagForced = value == 1;
+ break;
+ case ID_FLAG_FORCED:
+ currentTrack.flagDefault = value == 1;
+ break;
+ case ID_TRACK_TYPE:
+ currentTrack.type = (int) value;
+ break;
+ case ID_DEFAULT_DURATION:
+ currentTrack.defaultSampleDurationNs = (int) value;
+ break;
+ case ID_CODEC_DELAY:
+ currentTrack.codecDelayNs = value;
+ break;
+ case ID_SEEK_PRE_ROLL:
+ currentTrack.seekPreRollNs = value;
+ break;
+ case ID_CHANNELS:
+ currentTrack.channelCount = (int) value;
+ break;
+ case ID_AUDIO_BIT_DEPTH:
+ currentTrack.audioBitDepth = (int) value;
+ break;
+ case ID_REFERENCE_BLOCK:
+ sampleSeenReferenceBlock = true;
+ break;
+ case ID_CONTENT_ENCODING_ORDER:
+ // This extractor only supports one ContentEncoding element and hence the order has to be 0.
+ if (value != 0) {
+ throw new ParserException("ContentEncodingOrder " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCODING_SCOPE:
+ // This extractor only supports the scope of all frames.
+ if (value != 1) {
+ throw new ParserException("ContentEncodingScope " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_COMPRESSION_ALGORITHM:
+ // This extractor only supports header stripping.
+ if (value != 3) {
+ throw new ParserException("ContentCompAlgo " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCRYPTION_ALGORITHM:
+ // Only the value 5 (AES) is allowed according to the WebM specification.
+ if (value != 5) {
+ throw new ParserException("ContentEncAlgo " + value + " not supported");
+ }
+ break;
+ case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
+ // Only the value 1 is allowed according to the WebM specification.
+ if (value != 1) {
+ throw new ParserException("AESSettingsCipherMode " + value + " not supported");
+ }
+ break;
+ case ID_CUE_TIME:
+ cueTimesUs.add(scaleTimecodeToUs(value));
+ break;
+ case ID_CUE_CLUSTER_POSITION:
+ if (!seenClusterPositionForCurrentCuePoint) {
+ // If there's more than one video/audio track, then there could be more than one
+ // CueTrackPositions within a single CuePoint. In such a case, ignore all but the first
+ // one (since the cluster position will be quite close for all the tracks).
+ cueClusterPositions.add(value);
+ seenClusterPositionForCurrentCuePoint = true;
+ }
+ break;
+ case ID_TIME_CODE:
+ clusterTimecodeUs = scaleTimecodeToUs(value);
+ break;
+ case ID_BLOCK_DURATION:
+ blockDurationUs = scaleTimecodeToUs(value);
+ break;
+ case ID_STEREO_MODE:
+ int layout = (int) value;
+ switch (layout) {
+ case 0:
+ currentTrack.stereoMode = C.STEREO_MODE_MONO;
+ break;
+ case 1:
+ currentTrack.stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ break;
+ case 3:
+ currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ break;
+ case 15:
+ currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_PRIMARIES:
+ currentTrack.hasColorInfo = true;
+ switch ((int) value) {
+ case 1:
+ currentTrack.colorSpace = C.COLOR_SPACE_BT709;
+ break;
+ case 4: // BT.470M.
+ case 5: // BT.470BG.
+ case 6: // SMPTE 170M.
+ case 7: // SMPTE 240M.
+ currentTrack.colorSpace = C.COLOR_SPACE_BT601;
+ break;
+ case 9:
+ currentTrack.colorSpace = C.COLOR_SPACE_BT2020;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_TRANSFER:
+ switch ((int) value) {
+ case 1: // BT.709.
+ case 6: // SMPTE 170M.
+ case 7: // SMPTE 240M.
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_SDR;
+ break;
+ case 16:
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_ST2084;
+ break;
+ case 18:
+ currentTrack.colorTransfer = C.COLOR_TRANSFER_HLG;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_COLOUR_RANGE:
+ switch((int) value) {
+ case 1: // Broadcast range.
+ currentTrack.colorRange = C.COLOR_RANGE_LIMITED;
+ break;
+ case 2:
+ currentTrack.colorRange = C.COLOR_RANGE_FULL;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ID_MAX_CLL:
+ currentTrack.maxContentLuminance = (int) value;
+ break;
+ case ID_MAX_FALL:
+ currentTrack.maxFrameAverageLuminance = (int) value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* package */ void floatElement(int id, double value) {
+ switch (id) {
+ case ID_DURATION:
+ durationTimecode = (long) value;
+ break;
+ case ID_SAMPLING_FREQUENCY:
+ currentTrack.sampleRate = (int) value;
+ break;
+ case ID_PRIMARY_R_CHROMATICITY_X:
+ currentTrack.primaryRChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_R_CHROMATICITY_Y:
+ currentTrack.primaryRChromaticityY = (float) value;
+ break;
+ case ID_PRIMARY_G_CHROMATICITY_X:
+ currentTrack.primaryGChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_G_CHROMATICITY_Y:
+ currentTrack.primaryGChromaticityY = (float) value;
+ break;
+ case ID_PRIMARY_B_CHROMATICITY_X:
+ currentTrack.primaryBChromaticityX = (float) value;
+ break;
+ case ID_PRIMARY_B_CHROMATICITY_Y:
+ currentTrack.primaryBChromaticityY = (float) value;
+ break;
+ case ID_WHITE_POINT_CHROMATICITY_X:
+ currentTrack.whitePointChromaticityX = (float) value;
+ break;
+ case ID_WHITE_POINT_CHROMATICITY_Y:
+ currentTrack.whitePointChromaticityY = (float) value;
+ break;
+ case ID_LUMNINANCE_MAX:
+ currentTrack.maxMasteringLuminance = (float) value;
+ break;
+ case ID_LUMNINANCE_MIN:
+ currentTrack.minMasteringLuminance = (float) value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* package */ void stringElement(int id, String value) throws ParserException {
+ switch (id) {
+ case ID_DOC_TYPE:
+ // Validate that DocType is supported.
+ if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) {
+ throw new ParserException("DocType " + value + " not supported");
+ }
+ break;
+ case ID_CODEC_ID:
+ currentTrack.codecId = value;
+ break;
+ case ID_LANGUAGE:
+ currentTrack.language = value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* package */ void binaryElement(int id, int contentSize, ExtractorInput input)
+ throws IOException, InterruptedException {
+ switch (id) {
+ case ID_SEEK_ID:
+ Arrays.fill(seekEntryIdBytes.data, (byte) 0);
+ input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize);
+ seekEntryIdBytes.setPosition(0);
+ seekEntryId = (int) seekEntryIdBytes.readUnsignedInt();
+ break;
+ case ID_CODEC_PRIVATE:
+ currentTrack.codecPrivate = new byte[contentSize];
+ input.readFully(currentTrack.codecPrivate, 0, contentSize);
+ break;
+ case ID_PROJECTION_PRIVATE:
+ currentTrack.projectionData = new byte[contentSize];
+ input.readFully(currentTrack.projectionData, 0, contentSize);
+ break;
+ case ID_CONTENT_COMPRESSION_SETTINGS:
+ // This extractor only supports header stripping, so the payload is the stripped bytes.
+ currentTrack.sampleStrippedBytes = new byte[contentSize];
+ input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize);
+ break;
+ case ID_CONTENT_ENCRYPTION_KEY_ID:
+ currentTrack.encryptionKeyId = new byte[contentSize];
+ input.readFully(currentTrack.encryptionKeyId, 0, contentSize);
+ break;
+ case ID_SIMPLE_BLOCK:
+ case ID_BLOCK:
+ // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+ // and http://matroska.org/technical/specs/index.html#block_structure
+ // for info about how data is organized in SimpleBlock and Block elements respectively. They
+ // differ only in the way flags are specified.
+
+ if (blockState == BLOCK_STATE_START) {
+ blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8);
+ blockTrackNumberLength = varintReader.getLastLength();
+ blockDurationUs = C.TIME_UNSET;
+ blockState = BLOCK_STATE_HEADER;
+ scratch.reset();
+ }
+
+ Track track = tracks.get(blockTrackNumber);
+
+ // Ignore the block if we don't know about the track to which it belongs.
+ if (track == null) {
+ input.skipFully(contentSize - blockTrackNumberLength);
+ blockState = BLOCK_STATE_START;
+ return;
+ }
+
+ if (blockState == BLOCK_STATE_HEADER) {
+ // Read the relative timecode (2 bytes) and flags (1 byte).
+ readScratch(input, 3);
+ int lacing = (scratch.data[2] & 0x06) >> 1;
+ if (lacing == LACING_NONE) {
+ blockLacingSampleCount = 1;
+ blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1);
+ blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3;
+ } else {
+ if (id != ID_SIMPLE_BLOCK) {
+ throw new ParserException("Lacing only supported in SimpleBlocks.");
+ }
+
+ // Read the sample count (1 byte).
+ readScratch(input, 4);
+ blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1;
+ blockLacingSampleSizes =
+ ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount);
+ if (lacing == LACING_FIXED_SIZE) {
+ int blockLacingSampleSize =
+ (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount;
+ Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize);
+ } else if (lacing == LACING_XIPH) {
+ int totalSamplesSize = 0;
+ int headerSize = 4;
+ for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) {
+ blockLacingSampleSizes[sampleIndex] = 0;
+ int byteValue;
+ do {
+ readScratch(input, ++headerSize);
+ byteValue = scratch.data[headerSize - 1] & 0xFF;
+ blockLacingSampleSizes[sampleIndex] += byteValue;
+ } while (byteValue == 0xFF);
+ totalSamplesSize += blockLacingSampleSizes[sampleIndex];
+ }
+ blockLacingSampleSizes[blockLacingSampleCount - 1] =
+ contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+ } else if (lacing == LACING_EBML) {
+ int totalSamplesSize = 0;
+ int headerSize = 4;
+ for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) {
+ blockLacingSampleSizes[sampleIndex] = 0;
+ readScratch(input, ++headerSize);
+ if (scratch.data[headerSize - 1] == 0) {
+ throw new ParserException("No valid varint length mask found");
+ }
+ long readValue = 0;
+ for (int i = 0; i < 8; i++) {
+ int lengthMask = 1 << (7 - i);
+ if ((scratch.data[headerSize - 1] & lengthMask) != 0) {
+ int readPosition = headerSize - 1;
+ headerSize += i;
+ readScratch(input, headerSize);
+ readValue = (scratch.data[readPosition++] & 0xFF) & ~lengthMask;
+ while (readPosition < headerSize) {
+ readValue <<= 8;
+ readValue |= (scratch.data[readPosition++] & 0xFF);
+ }
+ // The first read value is the first size. Later values are signed offsets.
+ if (sampleIndex > 0) {
+ readValue -= (1L << (6 + i * 7)) - 1;
+ }
+ break;
+ }
+ }
+ if (readValue < Integer.MIN_VALUE || readValue > Integer.MAX_VALUE) {
+ throw new ParserException("EBML lacing sample size out of range.");
+ }
+ int intReadValue = (int) readValue;
+ blockLacingSampleSizes[sampleIndex] = sampleIndex == 0
+ ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue;
+ totalSamplesSize += blockLacingSampleSizes[sampleIndex];
+ }
+ blockLacingSampleSizes[blockLacingSampleCount - 1] =
+ contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+ } else {
+ // Lacing is always in the range 0--3.
+ throw new ParserException("Unexpected lacing value: " + lacing);
+ }
+ }
+
+ int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF);
+ blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode);
+ boolean isInvisible = (scratch.data[2] & 0x08) == 0x08;
+ boolean isKeyframe = track.type == TRACK_TYPE_AUDIO
+ || (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
+ blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0)
+ | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0);
+ blockState = BLOCK_STATE_DATA;
+ blockLacingSampleIndex = 0;
+ }
+
+ if (id == ID_SIMPLE_BLOCK) {
+ // For SimpleBlock, we have metadata for each sample here.
+ while (blockLacingSampleIndex < blockLacingSampleCount) {
+ writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);
+ long sampleTimeUs = this.blockTimeUs
+ + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;
+ commitSampleToOutput(track, sampleTimeUs);
+ blockLacingSampleIndex++;
+ }
+ blockState = BLOCK_STATE_START;
+ } else {
+ // For Block, we send the metadata at the end of the BlockGroup element since we'll know
+ // if the sample is a keyframe or not only at that point.
+ writeSampleData(input, track, blockLacingSampleSizes[0]);
+ }
+
+ break;
+ default:
+ throw new ParserException("Unexpected id: " + id);
+ }
+ }
+
+ private void commitSampleToOutput(Track track, long timeUs) {
+ if (CODEC_ID_SUBRIP.equals(track.codecId)) {
+ writeSubripSample(track);
+ }
+ track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId);
+ sampleRead = true;
+ resetSample();
+ }
+
+ private void resetSample() {
+ sampleBytesRead = 0;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ sampleEncodingHandled = false;
+ sampleSignalByteRead = false;
+ samplePartitionCountRead = false;
+ samplePartitionCount = 0;
+ sampleSignalByte = (byte) 0;
+ sampleInitializationVectorRead = false;
+ sampleStrippedBytes.reset();
+ }
+
+ /**
+ * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from
+ * the extractor input if necessary.
+ */
+ private void readScratch(ExtractorInput input, int requiredLength)
+ throws IOException, InterruptedException {
+ if (scratch.limit() >= requiredLength) {
+ return;
+ }
+ if (scratch.capacity() < requiredLength) {
+ scratch.reset(Arrays.copyOf(scratch.data, Math.max(scratch.data.length * 2, requiredLength)),
+ scratch.limit());
+ }
+ input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit());
+ scratch.setLimit(requiredLength);
+ }
+
+ private void writeSampleData(ExtractorInput input, Track track, int size)
+ throws IOException, InterruptedException {
+ if (CODEC_ID_SUBRIP.equals(track.codecId)) {
+ int sizeWithPrefix = SUBRIP_PREFIX.length + size;
+ if (subripSample.capacity() < sizeWithPrefix) {
+ // Initialize subripSample to contain the required prefix and have space to hold a subtitle
+ // twice as long as this one.
+ subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size);
+ }
+ input.readFully(subripSample.data, SUBRIP_PREFIX.length, size);
+ subripSample.setPosition(0);
+ subripSample.setLimit(sizeWithPrefix);
+ // Defer writing the data to the track output. We need to modify the sample data by setting
+ // the correct end timecode, which we might not have yet.
+ return;
+ }
+
+ TrackOutput output = track.output;
+ if (!sampleEncodingHandled) {
+ if (track.hasContentEncryption) {
+ // If the sample is encrypted, read its encryption signal byte and set the IV size.
+ // Clear the encrypted flag.
+ blockFlags &= ~C.BUFFER_FLAG_ENCRYPTED;
+ if (!sampleSignalByteRead) {
+ input.readFully(scratch.data, 0, 1);
+ sampleBytesRead++;
+ if ((scratch.data[0] & 0x80) == 0x80) {
+ throw new ParserException("Extension bit is set in signal byte");
+ }
+ sampleSignalByte = scratch.data[0];
+ sampleSignalByteRead = true;
+ }
+ boolean isEncrypted = (sampleSignalByte & 0x01) == 0x01;
+ if (isEncrypted) {
+ boolean hasSubsampleEncryption = (sampleSignalByte & 0x02) == 0x02;
+ blockFlags |= C.BUFFER_FLAG_ENCRYPTED;
+ if (!sampleInitializationVectorRead) {
+ input.readFully(encryptionInitializationVector.data, 0, ENCRYPTION_IV_SIZE);
+ sampleBytesRead += ENCRYPTION_IV_SIZE;
+ sampleInitializationVectorRead = true;
+ // Write the signal byte, containing the IV size and the subsample encryption flag.
+ scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00));
+ scratch.setPosition(0);
+ output.sampleData(scratch, 1);
+ sampleBytesWritten++;
+ // Write the IV.
+ encryptionInitializationVector.setPosition(0);
+ output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE);
+ sampleBytesWritten += ENCRYPTION_IV_SIZE;
+ }
+ if (hasSubsampleEncryption) {
+ if (!samplePartitionCountRead) {
+ input.readFully(scratch.data, 0, 1);
+ sampleBytesRead++;
+ scratch.setPosition(0);
+ samplePartitionCount = scratch.readUnsignedByte();
+ samplePartitionCountRead = true;
+ }
+ int samplePartitionDataSize = samplePartitionCount * 4;
+ scratch.reset(samplePartitionDataSize);
+ input.readFully(scratch.data, 0, samplePartitionDataSize);
+ sampleBytesRead += samplePartitionDataSize;
+ short subsampleCount = (short) (1 + (samplePartitionCount / 2));
+ int subsampleDataSize = 2 + 6 * subsampleCount;
+ if (encryptionSubsampleDataBuffer == null
+ || encryptionSubsampleDataBuffer.capacity() < subsampleDataSize) {
+ encryptionSubsampleDataBuffer = ByteBuffer.allocate(subsampleDataSize);
+ }
+ encryptionSubsampleDataBuffer.position(0);
+ encryptionSubsampleDataBuffer.putShort(subsampleCount);
+ // Loop through the partition offsets and write out the data in the way ExoPlayer
+ // wants it (ISO 23001-7 Part 7):
+ // 2 bytes - sub sample count.
+ // for each sub sample:
+ // 2 bytes - clear data size.
+ // 4 bytes - encrypted data size.
+ int partitionOffset = 0;
+ for (int i = 0; i < samplePartitionCount; i++) {
+ int previousPartitionOffset = partitionOffset;
+ partitionOffset = scratch.readUnsignedIntToInt();
+ if ((i % 2) == 0) {
+ encryptionSubsampleDataBuffer.putShort(
+ (short) (partitionOffset - previousPartitionOffset));
+ } else {
+ encryptionSubsampleDataBuffer.putInt(partitionOffset - previousPartitionOffset);
+ }
+ }
+ int finalPartitionSize = size - sampleBytesRead - partitionOffset;
+ if ((samplePartitionCount % 2) == 1) {
+ encryptionSubsampleDataBuffer.putInt(finalPartitionSize);
+ } else {
+ encryptionSubsampleDataBuffer.putShort((short) finalPartitionSize);
+ encryptionSubsampleDataBuffer.putInt(0);
+ }
+ encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize);
+ output.sampleData(encryptionSubsampleData, subsampleDataSize);
+ sampleBytesWritten += subsampleDataSize;
+ }
+ }
+ } else if (track.sampleStrippedBytes != null) {
+ // If the sample has header stripping, prepare to read/output the stripped bytes first.
+ sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length);
+ }
+ sampleEncodingHandled = true;
+ }
+ size += sampleStrippedBytes.limit();
+
+ if (CODEC_ID_H264.equals(track.codecId) || CODEC_ID_H265.equals(track.codecId)) {
+ // TODO: Deduplicate with Mp4Extractor.
+
+ // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
+ // they're only 1 or 2 bytes long.
+ byte[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength;
+ int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ while (sampleBytesRead < size) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // Read the NAL length so that we know where we find the next one.
+ readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff,
+ nalUnitLengthFieldLength);
+ nalLength.setPosition(0);
+ sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ sampleBytesWritten += 4;
+ } else {
+ // Write the payload of the NAL unit.
+ sampleCurrentNalBytesRemaining -=
+ readToOutput(input, output, sampleCurrentNalBytesRemaining);
+ }
+ }
+ } else {
+ while (sampleBytesRead < size) {
+ readToOutput(input, output, size - sampleBytesRead);
+ }
+ }
+
+ if (CODEC_ID_VORBIS.equals(track.codecId)) {
+ // Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the
+ // number of samples in the current page. This definition holds good only for Ogg and
+ // irrelevant for Matroska. So we always set this to -1 (the decoder will ignore this value if
+ // we set it to -1). The android platform media extractor [2] does the same.
+ // [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314
+ // [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474
+ vorbisNumPageSamples.setPosition(0);
+ output.sampleData(vorbisNumPageSamples, 4);
+ sampleBytesWritten += 4;
+ }
+ }
+
+ private void writeSubripSample(Track track) {
+ setSubripSampleEndTimecode(subripSample.data, blockDurationUs);
+ // Note: If we ever want to support DRM protected subtitles then we'll need to output the
+ // appropriate encryption data here.
+ track.output.sampleData(subripSample, subripSample.limit());
+ sampleBytesWritten += subripSample.limit();
+ }
+
+ private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) {
+ byte[] timeCodeData;
+ if (timeUs == C.TIME_UNSET) {
+ timeCodeData = SUBRIP_TIMECODE_EMPTY;
+ } else {
+ int hours = (int) (timeUs / 3600000000L);
+ timeUs -= (hours * 3600000000L);
+ int minutes = (int) (timeUs / 60000000);
+ timeUs -= (minutes * 60000000);
+ int seconds = (int) (timeUs / 1000000);
+ timeUs -= (seconds * 1000000);
+ int milliseconds = (int) (timeUs / 1000);
+ timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, "%02d:%02d:%02d,%03d", hours,
+ minutes, seconds, milliseconds));
+ }
+ System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
+ SUBRIP_TIMECODE_LENGTH);
+ }
+
+ /**
+ * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of
+ * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.
+ */
+ private void readToTarget(ExtractorInput input, byte[] target, int offset, int length)
+ throws IOException, InterruptedException {
+ int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft());
+ input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes);
+ if (pendingStrippedBytes > 0) {
+ sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes);
+ }
+ sampleBytesRead += length;
+ }
+
+ /**
+ * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either
+ * {@link #sampleStrippedBytes} or data read from {@code input}.
+ */
+ private int readToOutput(ExtractorInput input, TrackOutput output, int length)
+ throws IOException, InterruptedException {
+ int bytesRead;
+ int strippedBytesLeft = sampleStrippedBytes.bytesLeft();
+ if (strippedBytesLeft > 0) {
+ bytesRead = Math.min(length, strippedBytesLeft);
+ output.sampleData(sampleStrippedBytes, bytesRead);
+ } else {
+ bytesRead = output.sampleData(input, length, false);
+ }
+ sampleBytesRead += bytesRead;
+ sampleBytesWritten += bytesRead;
+ return bytesRead;
+ }
+
+ /**
+ * Builds a {@link SeekMap} from the recently gathered Cues information.
+ *
+ * @return The built {@link SeekMap}. The returned {@link SeekMap} may be unseekable if cues
+ * information was missing or incomplete.
+ */
+ private SeekMap buildSeekMap() {
+ if (segmentContentPosition == C.POSITION_UNSET || durationUs == C.TIME_UNSET
+ || cueTimesUs == null || cueTimesUs.size() == 0
+ || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) {
+ // Cues information is missing or incomplete.
+ cueTimesUs = null;
+ cueClusterPositions = null;
+ return new SeekMap.Unseekable(durationUs);
+ }
+ int cuePointsSize = cueTimesUs.size();
+ int[] sizes = new int[cuePointsSize];
+ long[] offsets = new long[cuePointsSize];
+ long[] durationsUs = new long[cuePointsSize];
+ long[] timesUs = new long[cuePointsSize];
+ for (int i = 0; i < cuePointsSize; i++) {
+ timesUs[i] = cueTimesUs.get(i);
+ offsets[i] = segmentContentPosition + cueClusterPositions.get(i);
+ }
+ for (int i = 0; i < cuePointsSize - 1; i++) {
+ sizes[i] = (int) (offsets[i + 1] - offsets[i]);
+ durationsUs[i] = timesUs[i + 1] - timesUs[i];
+ }
+ sizes[cuePointsSize - 1] =
+ (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]);
+ durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
+ cueTimesUs = null;
+ cueClusterPositions = null;
+ return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
+ }
+
+ /**
+ * Updates the position of the holder to Cues element's position if the extractor configuration
+ * permits use of master seek entry. After building Cues sets the holder's position back to where
+ * it was before.
+ *
+ * @param seekPosition The holder whose position will be updated.
+ * @param currentPosition Current position of the input.
+ * @return Whether the seek position was updated.
+ */
+ private boolean maybeSeekForCues(PositionHolder seekPosition, long currentPosition) {
+ if (seekForCues) {
+ seekPositionAfterBuildingCues = currentPosition;
+ seekPosition.position = cuesContentPosition;
+ seekForCues = false;
+ return true;
+ }
+ // After parsing Cues, seek back to original position if available. We will not do this unless
+ // we seeked to get to the Cues in the first place.
+ if (sentSeekMap && seekPositionAfterBuildingCues != C.POSITION_UNSET) {
+ seekPosition.position = seekPositionAfterBuildingCues;
+ seekPositionAfterBuildingCues = C.POSITION_UNSET;
+ return true;
+ }
+ return false;
+ }
+
+ private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException {
+ if (timecodeScale == C.TIME_UNSET) {
+ throw new ParserException("Can't scale timecode prior to timecodeScale being set.");
+ }
+ return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000);
+ }
+
+ private static boolean isCodecSupported(String codecId) {
+ return CODEC_ID_VP8.equals(codecId)
+ || CODEC_ID_VP9.equals(codecId)
+ || CODEC_ID_MPEG2.equals(codecId)
+ || CODEC_ID_MPEG4_SP.equals(codecId)
+ || CODEC_ID_MPEG4_ASP.equals(codecId)
+ || CODEC_ID_MPEG4_AP.equals(codecId)
+ || CODEC_ID_H264.equals(codecId)
+ || CODEC_ID_H265.equals(codecId)
+ || CODEC_ID_FOURCC.equals(codecId)
+ || CODEC_ID_THEORA.equals(codecId)
+ || CODEC_ID_OPUS.equals(codecId)
+ || CODEC_ID_VORBIS.equals(codecId)
+ || CODEC_ID_AAC.equals(codecId)
+ || CODEC_ID_MP2.equals(codecId)
+ || CODEC_ID_MP3.equals(codecId)
+ || CODEC_ID_AC3.equals(codecId)
+ || CODEC_ID_E_AC3.equals(codecId)
+ || CODEC_ID_TRUEHD.equals(codecId)
+ || CODEC_ID_DTS.equals(codecId)
+ || CODEC_ID_DTS_EXPRESS.equals(codecId)
+ || CODEC_ID_DTS_LOSSLESS.equals(codecId)
+ || CODEC_ID_FLAC.equals(codecId)
+ || CODEC_ID_ACM.equals(codecId)
+ || CODEC_ID_PCM_INT_LIT.equals(codecId)
+ || CODEC_ID_SUBRIP.equals(codecId)
+ || CODEC_ID_VOBSUB.equals(codecId)
+ || CODEC_ID_PGS.equals(codecId)
+ || CODEC_ID_DVBSUB.equals(codecId);
+ }
+
+ /**
+ * Returns an array that can store (at least) {@code length} elements, which will be either a new
+ * array or {@code array} if it's not null and large enough.
+ */
+ private static int[] ensureArrayCapacity(int[] array, int length) {
+ if (array == null) {
+ return new int[length];
+ } else if (array.length >= length) {
+ return array;
+ } else {
+ // Double the size to avoid allocating constantly if the required length increases gradually.
+ return new int[Math.max(array.length * 2, length)];
+ }
+ }
+
+ /**
+ * Passes events through to the outer {@link MatroskaExtractor}.
+ */
+ private final class InnerEbmlReaderOutput implements EbmlReaderOutput {
+
+ @Override
+ public int getElementType(int id) {
+ return MatroskaExtractor.this.getElementType(id);
+ }
+
+ @Override
+ public boolean isLevel1Element(int id) {
+ return MatroskaExtractor.this.isLevel1Element(id);
+ }
+
+ @Override
+ public void startMasterElement(int id, long contentPosition, long contentSize)
+ throws ParserException {
+ MatroskaExtractor.this.startMasterElement(id, contentPosition, contentSize);
+ }
+
+ @Override
+ public void endMasterElement(int id) throws ParserException {
+ MatroskaExtractor.this.endMasterElement(id);
+ }
+
+ @Override
+ public void integerElement(int id, long value) throws ParserException {
+ MatroskaExtractor.this.integerElement(id, value);
+ }
+
+ @Override
+ public void floatElement(int id, double value) throws ParserException {
+ MatroskaExtractor.this.floatElement(id, value);
+ }
+
+ @Override
+ public void stringElement(int id, String value) throws ParserException {
+ MatroskaExtractor.this.stringElement(id, value);
+ }
+
+ @Override
+ public void binaryElement(int id, int contentsSize, ExtractorInput input)
+ throws IOException, InterruptedException {
+ MatroskaExtractor.this.binaryElement(id, contentsSize, input);
+ }
+
+ }
+
+ private static final class Track {
+
+ private static final int DISPLAY_UNIT_PIXELS = 0;
+ private static final int MAX_CHROMATICITY = 50000; // Defined in CTA-861.3.
+ /**
+ * Default max content light level (CLL) that should be encoded into hdrStaticInfo.
+ */
+ private static final int DEFAULT_MAX_CLL = 1000; // nits.
+
+ /**
+ * Default frame-average light level (FALL) that should be encoded into hdrStaticInfo.
+ */
+ private static final int DEFAULT_MAX_FALL = 200; // nits.
+
+ // Common elements.
+ public String codecId;
+ public int number;
+ public int type;
+ public int defaultSampleDurationNs;
+ public boolean hasContentEncryption;
+ public byte[] sampleStrippedBytes;
+ public byte[] encryptionKeyId;
+ public byte[] codecPrivate;
+ public DrmInitData drmInitData;
+
+ // Video elements.
+ public int width = Format.NO_VALUE;
+ public int height = Format.NO_VALUE;
+ public int displayWidth = Format.NO_VALUE;
+ public int displayHeight = Format.NO_VALUE;
+ public int displayUnit = DISPLAY_UNIT_PIXELS;
+ public byte[] projectionData = null;
+ @C.StereoMode
+ public int stereoMode = Format.NO_VALUE;
+ public boolean hasColorInfo = false;
+ @C.ColorSpace
+ public int colorSpace = Format.NO_VALUE;
+ @C.ColorTransfer
+ public int colorTransfer = Format.NO_VALUE;
+ @C.ColorRange
+ public int colorRange = Format.NO_VALUE;
+ public int maxContentLuminance = DEFAULT_MAX_CLL;
+ public int maxFrameAverageLuminance = DEFAULT_MAX_FALL;
+ public float primaryRChromaticityX = Format.NO_VALUE;
+ public float primaryRChromaticityY = Format.NO_VALUE;
+ public float primaryGChromaticityX = Format.NO_VALUE;
+ public float primaryGChromaticityY = Format.NO_VALUE;
+ public float primaryBChromaticityX = Format.NO_VALUE;
+ public float primaryBChromaticityY = Format.NO_VALUE;
+ public float whitePointChromaticityX = Format.NO_VALUE;
+ public float whitePointChromaticityY = Format.NO_VALUE;
+ public float maxMasteringLuminance = Format.NO_VALUE;
+ public float minMasteringLuminance = Format.NO_VALUE;
+
+ // Audio elements. Initially set to their default values.
+ public int channelCount = 1;
+ public int audioBitDepth = Format.NO_VALUE;
+ public int sampleRate = 8000;
+ public long codecDelayNs = 0;
+ public long seekPreRollNs = 0;
+
+ // Text elements.
+ public boolean flagForced;
+ public boolean flagDefault = true;
+ private String language = "eng";
+
+ // Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265.
+ public TrackOutput output;
+ public int nalUnitLengthFieldLength;
+
+ /**
+ * Initializes the track with an output.
+ */
+ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException {
+ String mimeType;
+ int maxInputSize = Format.NO_VALUE;
+ @C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
+ List<byte[]> initializationData = null;
+ switch (codecId) {
+ case CODEC_ID_VP8:
+ mimeType = MimeTypes.VIDEO_VP8;
+ break;
+ case CODEC_ID_VP9:
+ mimeType = MimeTypes.VIDEO_VP9;
+ break;
+ case CODEC_ID_MPEG2:
+ mimeType = MimeTypes.VIDEO_MPEG2;
+ break;
+ case CODEC_ID_MPEG4_SP:
+ case CODEC_ID_MPEG4_ASP:
+ case CODEC_ID_MPEG4_AP:
+ mimeType = MimeTypes.VIDEO_MP4V;
+ initializationData =
+ codecPrivate == null ? null : Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_H264:
+ mimeType = MimeTypes.VIDEO_H264;
+ AvcConfig avcConfig = AvcConfig.parse(new ParsableByteArray(codecPrivate));
+ initializationData = avcConfig.initializationData;
+ nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ break;
+ case CODEC_ID_H265:
+ mimeType = MimeTypes.VIDEO_H265;
+ HevcConfig hevcConfig = HevcConfig.parse(new ParsableByteArray(codecPrivate));
+ initializationData = hevcConfig.initializationData;
+ nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
+ break;
+ case CODEC_ID_FOURCC:
+ initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate));
+ mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1;
+ break;
+ case CODEC_ID_THEORA:
+ // TODO: This can be set to the real mimeType if/when we work out what initializationData
+ // should be set to for this case.
+ mimeType = MimeTypes.VIDEO_UNKNOWN;
+ break;
+ case CODEC_ID_VORBIS:
+ mimeType = MimeTypes.AUDIO_VORBIS;
+ maxInputSize = VORBIS_MAX_INPUT_SIZE;
+ initializationData = parseVorbisCodecPrivate(codecPrivate);
+ break;
+ case CODEC_ID_OPUS:
+ mimeType = MimeTypes.AUDIO_OPUS;
+ maxInputSize = OPUS_MAX_INPUT_SIZE;
+ initializationData = new ArrayList<>(3);
+ initializationData.add(codecPrivate);
+ initializationData.add(
+ ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(codecDelayNs).array());
+ initializationData.add(
+ ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(seekPreRollNs).array());
+ break;
+ case CODEC_ID_AAC:
+ mimeType = MimeTypes.AUDIO_AAC;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_MP2:
+ mimeType = MimeTypes.AUDIO_MPEG_L2;
+ maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ break;
+ case CODEC_ID_MP3:
+ mimeType = MimeTypes.AUDIO_MPEG;
+ maxInputSize = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ break;
+ case CODEC_ID_AC3:
+ mimeType = MimeTypes.AUDIO_AC3;
+ break;
+ case CODEC_ID_E_AC3:
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ break;
+ case CODEC_ID_TRUEHD:
+ mimeType = MimeTypes.AUDIO_TRUEHD;
+ break;
+ case CODEC_ID_DTS:
+ case CODEC_ID_DTS_EXPRESS:
+ mimeType = MimeTypes.AUDIO_DTS;
+ break;
+ case CODEC_ID_DTS_LOSSLESS:
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ break;
+ case CODEC_ID_FLAC:
+ mimeType = MimeTypes.AUDIO_FLAC;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_ACM:
+ mimeType = MimeTypes.AUDIO_RAW;
+ if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) {
+ throw new ParserException("Non-PCM MS/ACM is unsupported");
+ }
+ pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+ if (pcmEncoding == C.ENCODING_INVALID) {
+ throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
+ }
+ break;
+ case CODEC_ID_PCM_INT_LIT:
+ mimeType = MimeTypes.AUDIO_RAW;
+ pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+ if (pcmEncoding == C.ENCODING_INVALID) {
+ throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
+ }
+ break;
+ case CODEC_ID_SUBRIP:
+ mimeType = MimeTypes.APPLICATION_SUBRIP;
+ break;
+ case CODEC_ID_VOBSUB:
+ mimeType = MimeTypes.APPLICATION_VOBSUB;
+ initializationData = Collections.singletonList(codecPrivate);
+ break;
+ case CODEC_ID_PGS:
+ mimeType = MimeTypes.APPLICATION_PGS;
+ break;
+ case CODEC_ID_DVBSUB:
+ mimeType = MimeTypes.APPLICATION_DVBSUBS;
+ // Init data: composition_page (2), ancillary_page (2)
+ initializationData = Collections.singletonList(new byte[] {codecPrivate[0],
+ codecPrivate[1], codecPrivate[2], codecPrivate[3]});
+ break;
+ default:
+ throw new ParserException("Unrecognized codec identifier.");
+ }
+
+ int type;
+ Format format;
+ @C.SelectionFlags int selectionFlags = 0;
+ selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
+ selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;
+ // TODO: Consider reading the name elements of the tracks and, if present, incorporating them
+ // into the trackId passed when creating the formats.
+ if (MimeTypes.isAudio(mimeType)) {
+ type = C.TRACK_TYPE_AUDIO;
+ format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
+ initializationData, drmInitData, selectionFlags, language);
+ } else if (MimeTypes.isVideo(mimeType)) {
+ type = C.TRACK_TYPE_VIDEO;
+ if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
+ displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
+ displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
+ }
+ float pixelWidthHeightRatio = Format.NO_VALUE;
+ if (displayWidth != Format.NO_VALUE && displayHeight != Format.NO_VALUE) {
+ pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight);
+ }
+ ColorInfo colorInfo = null;
+ if (hasColorInfo) {
+ byte[] hdrStaticInfo = getHdrStaticInfo();
+ colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
+ }
+ format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
+ Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo,
+ drmInitData);
+ } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
+ type = C.TRACK_TYPE_TEXT;
+ format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, selectionFlags, language, drmInitData);
+ } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
+ || MimeTypes.APPLICATION_PGS.equals(mimeType)
+ || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {
+ type = C.TRACK_TYPE_TEXT;
+ format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, initializationData, language, drmInitData);
+ } else {
+ throw new ParserException("Unexpected MIME type.");
+ }
+
+ this.output = output.track(number, type);
+ this.output.format(format);
+ }
+
+ /**
+ * Returns the HDR Static Info as defined in CTA-861.3.
+ */
+ private byte[] getHdrStaticInfo() {
+ // Are all fields present.
+ if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE
+ || primaryGChromaticityX == Format.NO_VALUE || primaryGChromaticityY == Format.NO_VALUE
+ || primaryBChromaticityX == Format.NO_VALUE || primaryBChromaticityY == Format.NO_VALUE
+ || whitePointChromaticityX == Format.NO_VALUE
+ || whitePointChromaticityY == Format.NO_VALUE || maxMasteringLuminance == Format.NO_VALUE
+ || minMasteringLuminance == Format.NO_VALUE) {
+ return null;
+ }
+
+ byte[] hdrStaticInfoData = new byte[25];
+ ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData);
+ hdrStaticInfo.put((byte) 0); // Type.
+ hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryGChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((primaryBChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityX * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) ((whitePointChromaticityY * MAX_CHROMATICITY) + 0.5f));
+ hdrStaticInfo.putShort((short) (maxMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) (minMasteringLuminance + 0.5f));
+ hdrStaticInfo.putShort((short) maxContentLuminance);
+ hdrStaticInfo.putShort((short) maxFrameAverageLuminance);
+ return hdrStaticInfoData;
+ }
+
+ /**
+ * Builds initialization data for a {@link Format} from FourCC codec private data.
+ * <p>
+ * VC1 is the only supported compression type.
+ *
+ * @return The initialization data for the {@link Format}, or null if the compression type is
+ * not VC1.
+ * @throws ParserException If the initialization data could not be built.
+ */
+ private static List<byte[]> parseFourCcVc1Private(ParsableByteArray buffer)
+ throws ParserException {
+ try {
+ buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2).
+ long compression = buffer.readLittleEndianUnsignedInt();
+ if (compression != FOURCC_COMPRESSION_VC1) {
+ return null;
+ }
+
+ // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20
+ // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4).
+ int startOffset = buffer.getPosition() + 20;
+ byte[] bufferData = buffer.data;
+ for (int offset = startOffset; offset < bufferData.length - 4; offset++) {
+ if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00
+ && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) {
+ // We've found the initialization data.
+ byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length);
+ return Collections.singletonList(initializationData);
+ }
+ }
+
+ throw new ParserException("Failed to find FourCC VC1 initialization data");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing FourCC VC1 codec private");
+ }
+ }
+
+ /**
+ * Builds initialization data for a {@link Format} from Vorbis codec private data.
+ *
+ * @return The initialization data for the {@link Format}.
+ * @throws ParserException If the initialization data could not be built.
+ */
+ private static List<byte[]> parseVorbisCodecPrivate(byte[] codecPrivate)
+ throws ParserException {
+ try {
+ if (codecPrivate[0] != 0x02) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ int offset = 1;
+ int vorbisInfoLength = 0;
+ while (codecPrivate[offset] == (byte) 0xFF) {
+ vorbisInfoLength += 0xFF;
+ offset++;
+ }
+ vorbisInfoLength += codecPrivate[offset++];
+
+ int vorbisSkipLength = 0;
+ while (codecPrivate[offset] == (byte) 0xFF) {
+ vorbisSkipLength += 0xFF;
+ offset++;
+ }
+ vorbisSkipLength += codecPrivate[offset++];
+
+ if (codecPrivate[offset] != 0x01) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ byte[] vorbisInfo = new byte[vorbisInfoLength];
+ System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);
+ offset += vorbisInfoLength;
+ if (codecPrivate[offset] != 0x03) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ offset += vorbisSkipLength;
+ if (codecPrivate[offset] != 0x05) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ byte[] vorbisBooks = new byte[codecPrivate.length - offset];
+ System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);
+ List<byte[]> initializationData = new ArrayList<>(2);
+ initializationData.add(vorbisInfo);
+ initializationData.add(vorbisBooks);
+ return initializationData;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing vorbis codec private");
+ }
+ }
+
+ /**
+ * Parses an MS/ACM codec private, returning whether it indicates PCM audio.
+ *
+ * @return Whether the codec private indicates PCM audio.
+ * @throws ParserException If a parsing error occurs.
+ */
+ private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException {
+ try {
+ int formatTag = buffer.readLittleEndianUnsignedShort();
+ if (formatTag == WAVE_FORMAT_PCM) {
+ return true;
+ } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) {
+ buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4)
+ return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits()
+ && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits();
+ } else {
+ return false;
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing MS/ACM codec private");
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
@@ -0,0 +1,113 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Utility class that peeks from the input stream in order to determine whether it appears to be
+ * compatible input for this extractor.
+ */
+/* package */ final class Sniffer {
+
+ /**
+ * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}.
+ */
+ private static final int SEARCH_LENGTH = 1024;
+ private static final int ID_EBML = 0x1A45DFA3;
+
+ private final ParsableByteArray scratch;
+ private int peekLength;
+
+ public Sniffer() {
+ scratch = new ParsableByteArray(8);
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.extractor.Extractor#sniff(ExtractorInput)
+ */
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
+ ? SEARCH_LENGTH : inputLength);
+ // Find four bytes equal to ID_EBML near the start of the input.
+ input.peekFully(scratch.data, 0, 4);
+ long tag = scratch.readUnsignedInt();
+ peekLength = 4;
+ while (tag != ID_EBML) {
+ if (++peekLength == bytesToSearch) {
+ return false;
+ }
+ input.peekFully(scratch.data, 0, 1);
+ tag = (tag << 8) & 0xFFFFFF00;
+ tag |= scratch.data[0] & 0xFF;
+ }
+
+ // Read the size of the EBML header and make sure it is within the stream.
+ long headerSize = readUint(input);
+ long headerStart = peekLength;
+ if (headerSize == Long.MIN_VALUE
+ || (inputLength != C.LENGTH_UNSET && headerStart + headerSize >= inputLength)) {
+ return false;
+ }
+
+ // Read the payload elements in the EBML header.
+ while (peekLength < headerStart + headerSize) {
+ long id = readUint(input);
+ if (id == Long.MIN_VALUE) {
+ return false;
+ }
+ long size = readUint(input);
+ if (size < 0 || size > Integer.MAX_VALUE) {
+ return false;
+ }
+ if (size != 0) {
+ input.advancePeekPosition((int) size);
+ peekLength += size;
+ }
+ }
+ return peekLength == headerStart + headerSize;
+ }
+
+ /**
+ * Peeks a variable-length unsigned EBML integer from the input.
+ */
+ private long readUint(ExtractorInput input) throws IOException, InterruptedException {
+ input.peekFully(scratch.data, 0, 1);
+ int value = scratch.data[0] & 0xFF;
+ if (value == 0) {
+ return Long.MIN_VALUE;
+ }
+ int mask = 0x80;
+ int length = 0;
+ while ((value & mask) == 0) {
+ mask >>= 1;
+ length++;
+ }
+ value &= ~mask;
+ input.peekFully(scratch.data, 1, length);
+ for (int i = 0; i < length; i++) {
+ value <<= 8;
+ value += scratch.data[i + 1] & 0xFF;
+ }
+ peekLength += length + 1;
+ return value;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mkv/VarintReader.java
@@ -0,0 +1,155 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}.
+ */
+/* package */ final class VarintReader {
+
+ private static final int STATE_BEGIN_READING = 0;
+ private static final int STATE_READ_CONTENTS = 1;
+
+ /**
+ * The first byte of a variable-length integer (varint) will have one of these bit masks
+ * indicating the total length in bytes.
+ *
+ * <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
+ */
+ private static final long[] VARINT_LENGTH_MASKS = new long[] {
+ 0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L
+ };
+
+ private final byte[] scratch;
+
+ private int state;
+ private int length;
+
+ public VarintReader() {
+ scratch = new byte[8];
+ }
+
+ /**
+ * Resets the reader to start reading a new variable-length integer.
+ */
+ public void reset() {
+ state = STATE_BEGIN_READING;
+ length = 0;
+ }
+
+ /**
+ * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that
+ * reading can be resumed later if an error occurs having read only some of it.
+ * <p>
+ * If an value is successfully read, then the reader will automatically reset itself ready to
+ * read another value.
+ * <p>
+ * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed
+ * later by calling this method again, passing an {@link ExtractorInput} providing data starting
+ * where the previous one left off.
+ *
+ * @param input The {@link ExtractorInput} from which the integer should be read.
+ * @param allowEndOfInput True if encountering the end of the input having read no data is
+ * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
+ * should be considered an error, causing an {@link EOFException} to be thrown.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @param maximumAllowedLength Maximum allowed length of the variable integer to be read.
+ * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true
+ * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the
+ * length of the varint exceeded maximumAllowedLength.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput,
+ boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException {
+ if (state == STATE_BEGIN_READING) {
+ // Read the first byte to establish the length.
+ if (!input.readFully(scratch, 0, 1, allowEndOfInput)) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int firstByte = scratch[0] & 0xFF;
+ length = parseUnsignedVarintLength(firstByte);
+ if (length == C.LENGTH_UNSET) {
+ throw new IllegalStateException("No valid varint length mask found");
+ }
+ state = STATE_READ_CONTENTS;
+ }
+
+ if (length > maximumAllowedLength) {
+ state = STATE_BEGIN_READING;
+ return C.RESULT_MAX_LENGTH_EXCEEDED;
+ }
+
+ if (length != 1) {
+ // Read the remaining bytes.
+ input.readFully(scratch, 1, length - 1);
+ }
+
+ state = STATE_BEGIN_READING;
+ return assembleVarint(scratch, length, removeLengthMask);
+ }
+
+ /**
+ * Returns the number of bytes occupied by the most recently parsed varint.
+ */
+ public int getLastLength() {
+ return length;
+ }
+
+ /**
+ * Parses and the length of the varint given the first byte.
+ *
+ * @param firstByte First byte of the varint.
+ * @return Length of the varint beginning with the given byte if it was valid,
+ * {@link C#LENGTH_UNSET} otherwise.
+ */
+ public static int parseUnsignedVarintLength(int firstByte) {
+ int varIntLength = C.LENGTH_UNSET;
+ for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
+ if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
+ varIntLength = i + 1;
+ break;
+ }
+ }
+ return varIntLength;
+ }
+
+ /**
+ * Assemble a varint from the given byte array.
+ *
+ * @param varintBytes Bytes that make up the varint.
+ * @param varintLength Length of the varint to assemble.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @return Parsed and assembled varint.
+ */
+ public static long assembleVarint(byte[] varintBytes, int varintLength,
+ boolean removeLengthMask) {
+ long varint = varintBytes[0] & 0xFFL;
+ if (removeLengthMask) {
+ varint &= ~VARINT_LENGTH_MASKS[varintLength - 1];
+ }
+ for (int i = 1; i < varintLength; i++) {
+ varint = (varint << 8) | (varintBytes[i] & 0xFFL);
+ }
+ return varint;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
+ */
+/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker {
+
+ private static final int BITS_PER_BYTE = 8;
+
+ private final long firstFramePosition;
+ private final int bitrate;
+ private final long durationUs;
+
+ public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) {
+ this.firstFramePosition = firstFramePosition;
+ this.bitrate = bitrate;
+ durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength);
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return durationUs != C.TIME_UNSET;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return durationUs == C.TIME_UNSET ? 0
+ : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE)
+ / bitrate;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -0,0 +1,420 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp3;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts data from an MP3 file.
+ */
+public final class Mp3Extractor implements Extractor {
+
+ /**
+ * Factory for {@link Mp3Extractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new Mp3Extractor()};
+ }
+
+ };
+
+ /**
+ * Flags controlling the behavior of the extractor.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA})
+ public @interface Flags {}
+ /**
+ * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
+ * otherwise not be possible.
+ */
+ public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
+ /**
+ * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
+ * required.
+ */
+ public static final int FLAG_DISABLE_ID3_METADATA = 2;
+
+ /**
+ * The maximum number of bytes to search when synchronizing, before giving up.
+ */
+ private static final int MAX_SYNC_BYTES = 128 * 1024;
+ /**
+ * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up.
+ */
+ private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+ /**
+ * Maximum length of data read into {@link #scratch}.
+ */
+ private static final int SCRATCH_LENGTH = 10;
+
+ /**
+ * Mask that includes the audio header values that must match between frames.
+ */
+ private static final int HEADER_MASK = 0xFFFE0C00;
+ private static final int XING_HEADER = Util.getIntegerCodeForString("Xing");
+ private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
+ private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
+
+ @Flags private final int flags;
+ private final long forcedFirstSampleTimestampUs;
+ private final ParsableByteArray scratch;
+ private final MpegAudioHeader synchronizedHeader;
+ private final GaplessInfoHolder gaplessInfoHolder;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+ private TrackOutput trackOutput;
+
+ private int synchronizedHeaderData;
+
+ private Metadata metadata;
+ private Seeker seeker;
+ private long basisTimeUs;
+ private long samplesRead;
+ private int sampleBytesRemaining;
+
+ /**
+ * Constructs a new {@link Mp3Extractor}.
+ */
+ public Mp3Extractor() {
+ this(0);
+ }
+
+ /**
+ * Constructs a new {@link Mp3Extractor}.
+ *
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public Mp3Extractor(@Flags int flags) {
+ this(flags, C.TIME_UNSET);
+ }
+
+ /**
+ * Constructs a new {@link Mp3Extractor}.
+ *
+ * @param flags Flags that control the extractor's behavior.
+ * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
+ * {@link C#TIME_UNSET} if forcing is not required.
+ */
+ public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
+ this.flags = flags;
+ this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
+ scratch = new ParsableByteArray(SCRATCH_LENGTH);
+ synchronizedHeader = new MpegAudioHeader();
+ gaplessInfoHolder = new GaplessInfoHolder();
+ basisTimeUs = C.TIME_UNSET;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return synchronize(input, true);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
+ extractorOutput.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ synchronizedHeaderData = 0;
+ basisTimeUs = C.TIME_UNSET;
+ samplesRead = 0;
+ sampleBytesRemaining = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (synchronizedHeaderData == 0) {
+ try {
+ synchronize(input, false);
+ } catch (EOFException e) {
+ return RESULT_END_OF_INPUT;
+ }
+ }
+ if (seeker == null) {
+ seeker = setupSeeker(input);
+ extractorOutput.seekMap(seeker);
+ trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
+ Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
+ synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
+ gaplessInfoHolder.encoderPadding, null, null, 0, null,
+ (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
+ }
+ return readSample(input);
+ }
+
+ private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
+ if (sampleBytesRemaining == 0) {
+ extractorInput.resetPeekPosition();
+ if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
+ return RESULT_END_OF_INPUT;
+ }
+ scratch.setPosition(0);
+ int sampleHeaderData = scratch.readInt();
+ if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)
+ || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {
+ // We have lost synchronization, so attempt to resynchronize starting at the next byte.
+ extractorInput.skipFully(1);
+ synchronizedHeaderData = 0;
+ return RESULT_CONTINUE;
+ }
+ MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
+ if (basisTimeUs == C.TIME_UNSET) {
+ basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());
+ if (forcedFirstSampleTimestampUs != C.TIME_UNSET) {
+ long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0);
+ basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs;
+ }
+ }
+ sampleBytesRemaining = synchronizedHeader.frameSize;
+ }
+ int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true);
+ if (bytesAppended == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ sampleBytesRemaining -= bytesAppended;
+ if (sampleBytesRemaining > 0) {
+ return RESULT_CONTINUE;
+ }
+ long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate);
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0,
+ null);
+ samplesRead += synchronizedHeader.samplesPerFrame;
+ sampleBytesRemaining = 0;
+ return RESULT_CONTINUE;
+ }
+
+ private boolean synchronize(ExtractorInput input, boolean sniffing)
+ throws IOException, InterruptedException {
+ int validFrameCount = 0;
+ int candidateSynchronizedHeaderData = 0;
+ int peekedId3Bytes = 0;
+ int searchedBytes = 0;
+ int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
+ input.resetPeekPosition();
+ if (input.getPosition() == 0) {
+ peekId3Data(input);
+ peekedId3Bytes = (int) input.getPeekPosition();
+ if (!sniffing) {
+ input.skipFully(peekedId3Bytes);
+ }
+ }
+ while (true) {
+ if (!input.peekFully(scratch.data, 0, 4, validFrameCount > 0)) {
+ // We reached the end of the stream but found at least one valid frame.
+ break;
+ }
+ scratch.setPosition(0);
+ int headerData = scratch.readInt();
+ int frameSize;
+ if ((candidateSynchronizedHeaderData != 0
+ && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
+ || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {
+ // The header doesn't match the candidate header or is invalid. Try the next byte offset.
+ if (searchedBytes++ == searchLimitBytes) {
+ if (!sniffing) {
+ throw new ParserException("Searched too many bytes.");
+ }
+ return false;
+ }
+ validFrameCount = 0;
+ candidateSynchronizedHeaderData = 0;
+ if (sniffing) {
+ input.resetPeekPosition();
+ input.advancePeekPosition(peekedId3Bytes + searchedBytes);
+ } else {
+ input.skipFully(1);
+ }
+ } else {
+ // The header matches the candidate header and/or is valid.
+ validFrameCount++;
+ if (validFrameCount == 1) {
+ MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
+ candidateSynchronizedHeaderData = headerData;
+ } else if (validFrameCount == 4) {
+ break;
+ }
+ input.advancePeekPosition(frameSize - 4);
+ }
+ }
+ // Prepare to read the synchronized frame.
+ if (sniffing) {
+ input.skipFully(peekedId3Bytes + searchedBytes);
+ } else {
+ input.resetPeekPosition();
+ }
+ synchronizedHeaderData = candidateSynchronizedHeaderData;
+ return true;
+ }
+
+ /**
+ * Peeks ID3 data from the input, including gapless playback information.
+ *
+ * @param input The {@link ExtractorInput} from which data should be peeked.
+ * @throws IOException If an error occurred peeking from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
+ int peekedId3Bytes = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
+ // Not an ID3 tag.
+ break;
+ }
+ scratch.skipBytes(3); // Skip major version, minor version and flags.
+ int framesLength = scratch.readSynchSafeInt();
+ int tagLength = Id3Decoder.ID3_HEADER_LENGTH + framesLength;
+
+ if (metadata == null) {
+ byte[] id3Data = new byte[tagLength];
+ System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
+ // We need to parse enough ID3 metadata to retrieve any gapless playback information even
+ // if ID3 metadata parsing is disabled.
+ Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0
+ ? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
+ metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
+ if (metadata != null) {
+ gaplessInfoHolder.setFromMetadata(metadata);
+ }
+ } else {
+ input.advancePeekPosition(framesLength);
+ }
+
+ peekedId3Bytes += tagLength;
+ }
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(peekedId3Bytes);
+ }
+
+ /**
+ * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
+ * data from the start of the first frame in the stream. On returning, the input's position will
+ * be set to the start of the first frame of audio.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @throws IOException Thrown if there was an error reading from the stream. Not expected if the
+ * next two frames were already peeked during synchronization.
+ * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
+ * the next two frames were already peeked during synchronization.
+ * @return a {@link Seeker}.
+ */
+ private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
+ // Read the first frame which may contain a Xing or VBRI header with seeking metadata.
+ ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
+ input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
+
+ long position = input.getPosition();
+ long length = input.getLength();
+ int headerData = 0;
+ Seeker seeker = null;
+
+ // Check if there is a Xing header.
+ int xingBase = (synchronizedHeader.version & 1) != 0
+ ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
+ : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
+ if (frame.limit() >= xingBase + 4) {
+ frame.setPosition(xingBase);
+ headerData = frame.readInt();
+ }
+ if (headerData == XING_HEADER || headerData == INFO_HEADER) {
+ seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
+ if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
+ // If there is a Xing header, read gapless playback metadata at a fixed offset.
+ input.resetPeekPosition();
+ input.advancePeekPosition(xingBase + 141);
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
+ }
+ input.skipFully(synchronizedHeader.frameSize);
+ } else if (frame.limit() >= 40) {
+ // Check if there is a VBRI header.
+ frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
+ headerData = frame.readInt();
+ if (headerData == VBRI_HEADER) {
+ seeker = VbriSeeker.create(synchronizedHeader, frame, position, length);
+ input.skipFully(synchronizedHeader.frameSize);
+ }
+ }
+
+ if (seeker == null || (!seeker.isSeekable()
+ && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
+ // Repopulate the synchronized header in case we had to skip an invalid seeking header, which
+ // would give an invalid CBR bitrate.
+ input.resetPeekPosition();
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
+ seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length);
+ }
+
+ return seeker;
+ }
+
+ /**
+ * {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
+ * used to work out the new sample basis timestamp after seeking and resynchronization.
+ */
+ /* package */ interface Seeker extends SeekMap {
+
+ /**
+ * Maps a position (byte offset) to a corresponding sample timestamp.
+ *
+ * @param position A seek position (byte offset) relative to the start of the stream.
+ * @return The corresponding timestamp of the next sample to be read, in microseconds.
+ */
+ long getTimeUs(long position);
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
@@ -0,0 +1,120 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * MP3 seeker that uses metadata from a VBRI header.
+ */
+/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
+
+ /**
+ * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present.
+ * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
+ * caller should reset it.
+ *
+ * @param mpegAudioHeader The MPEG audio header associated with the frame.
+ * @param frame The data in this audio frame, with its position set to immediately after the
+ * 'VBRI' tag.
+ * @param position The position (byte offset) of the start of this frame in the stream.
+ * @param inputLength The length of the stream in bytes.
+ * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
+ * information is not present.
+ */
+ public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
+ long position, long inputLength) {
+ frame.skipBytes(10);
+ int numFrames = frame.readInt();
+ if (numFrames <= 0) {
+ return null;
+ }
+ int sampleRate = mpegAudioHeader.sampleRate;
+ long durationUs = Util.scaleLargeTimestamp(numFrames,
+ C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate);
+ int entryCount = frame.readUnsignedShort();
+ int scale = frame.readUnsignedShort();
+ int entrySize = frame.readUnsignedShort();
+ frame.skipBytes(2);
+
+ // Skip the frame containing the VBRI header.
+ position += mpegAudioHeader.frameSize;
+
+ // Read table of contents entries.
+ long[] timesUs = new long[entryCount + 1];
+ long[] positions = new long[entryCount + 1];
+ timesUs[0] = 0L;
+ positions[0] = position;
+ for (int index = 1; index < timesUs.length; index++) {
+ int segmentSize;
+ switch (entrySize) {
+ case 1:
+ segmentSize = frame.readUnsignedByte();
+ break;
+ case 2:
+ segmentSize = frame.readUnsignedShort();
+ break;
+ case 3:
+ segmentSize = frame.readUnsignedInt24();
+ break;
+ case 4:
+ segmentSize = frame.readUnsignedIntToInt();
+ break;
+ default:
+ return null;
+ }
+ position += segmentSize * scale;
+ timesUs[index] = index * durationUs / entryCount;
+ positions[index] =
+ inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position);
+ }
+ return new VbriSeeker(timesUs, positions, durationUs);
+ }
+
+ private final long[] timesUs;
+ private final long[] positions;
+ private final long durationUs;
+
+ private VbriSeeker(long[] timesUs, long[] positions, long durationUs) {
+ this.timesUs = timesUs;
+ this.positions = positions;
+ this.durationUs = durationUs;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)];
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ return timesUs[Util.binarySearchFloor(positions, position, true, true)];
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
@@ -0,0 +1,168 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * MP3 seeker that uses metadata from a Xing header.
+ */
+/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
+
+ /**
+ * Returns a {@link XingSeeker} for seeking in the stream, if required information is present.
+ * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
+ * caller should reset it.
+ *
+ * @param mpegAudioHeader The MPEG audio header associated with the frame.
+ * @param frame The data in this audio frame, with its position set to immediately after the
+ * 'Xing' or 'Info' tag.
+ * @param position The position (byte offset) of the start of this frame in the stream.
+ * @param inputLength The length of the stream in bytes.
+ * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
+ * information is not present.
+ */
+ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
+ long position, long inputLength) {
+ int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
+ int sampleRate = mpegAudioHeader.sampleRate;
+ long firstFramePosition = position + mpegAudioHeader.frameSize;
+
+ int flags = frame.readInt();
+ int frameCount;
+ if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) {
+ // If the frame count is missing/invalid, the header can't be used to determine the duration.
+ return null;
+ }
+ long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND,
+ sampleRate);
+ if ((flags & 0x06) != 0x06) {
+ // If the size in bytes or table of contents is missing, the stream is not seekable.
+ return new XingSeeker(firstFramePosition, durationUs, inputLength);
+ }
+
+ long sizeBytes = frame.readUnsignedIntToInt();
+ frame.skipBytes(1);
+ long[] tableOfContents = new long[99];
+ for (int i = 0; i < 99; i++) {
+ tableOfContents[i] = frame.readUnsignedByte();
+ }
+
+ // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
+ // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
+ // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
+ return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents,
+ sizeBytes, mpegAudioHeader.frameSize);
+ }
+
+ private final long firstFramePosition;
+ private final long durationUs;
+ private final long inputLength;
+ /**
+ * Entries are in the range [0, 255], but are stored as long integers for convenience.
+ */
+ private final long[] tableOfContents;
+ private final long sizeBytes;
+ private final int headerSize;
+
+ private XingSeeker(long firstFramePosition, long durationUs, long inputLength) {
+ this(firstFramePosition, durationUs, inputLength, null, 0, 0);
+ }
+
+ private XingSeeker(long firstFramePosition, long durationUs, long inputLength,
+ long[] tableOfContents, long sizeBytes, int headerSize) {
+ this.firstFramePosition = firstFramePosition;
+ this.durationUs = durationUs;
+ this.inputLength = inputLength;
+ this.tableOfContents = tableOfContents;
+ this.sizeBytes = sizeBytes;
+ this.headerSize = headerSize;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return tableOfContents != null;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ if (!isSeekable()) {
+ return firstFramePosition;
+ }
+ float percent = timeUs * 100f / durationUs;
+ float fx;
+ if (percent <= 0f) {
+ fx = 0f;
+ } else if (percent >= 100f) {
+ fx = 256f;
+ } else {
+ int a = (int) percent;
+ float fa, fb;
+ if (a == 0) {
+ fa = 0f;
+ } else {
+ fa = tableOfContents[a - 1];
+ }
+ if (a < 99) {
+ fb = tableOfContents[a];
+ } else {
+ fb = 256f;
+ }
+ fx = fa + (fb - fa) * (percent - a);
+ }
+
+ long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition;
+ long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1
+ : firstFramePosition - headerSize + sizeBytes - 1;
+ return Math.min(position, maximumPosition);
+ }
+
+ @Override
+ public long getTimeUs(long position) {
+ if (!isSeekable() || position < firstFramePosition) {
+ return 0L;
+ }
+ double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes;
+ int previousTocPosition =
+ Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1;
+ long previousTime = getTimeUsForTocPosition(previousTocPosition);
+
+ // Linearly interpolate the time taking into account the next entry.
+ long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1];
+ long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition];
+ long nextTime = getTimeUsForTocPosition(previousTocPosition + 1);
+ long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime)
+ * (offsetByte - previousByte) / (nextByte - previousByte));
+ return previousTime + timeOffset;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ /**
+ * Returns the time in microseconds corresponding to a table of contents position, which is
+ * interpreted as a percentage of the stream's duration between 0 and 100.
+ */
+ private long getTimeUsForTocPosition(int tocPosition) {
+ return durationUs * tocPosition / 100;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -0,0 +1,311 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/* package*/ abstract class Atom {
+
+ /**
+ * Size of an atom header, in bytes.
+ */
+ public static final int HEADER_SIZE = 8;
+
+ /**
+ * Size of a full atom header, in bytes.
+ */
+ public static final int FULL_HEADER_SIZE = 12;
+
+ /**
+ * Size of a long atom header, in bytes.
+ */
+ public static final int LONG_HEADER_SIZE = 16;
+
+ /**
+ * Value for the first 32 bits of atomSize when the atom size is actually a long value.
+ */
+ public static final int LONG_SIZE_PREFIX = 1;
+
+ public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
+ public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
+ public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3");
+ public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1");
+ public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1");
+ public static final int TYPE_s263 = Util.getIntegerCodeForString("s263");
+ public static final int TYPE_d263 = Util.getIntegerCodeForString("d263");
+ public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat");
+ public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a");
+ public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3");
+ public static final int TYPE_wave = Util.getIntegerCodeForString("wave");
+ public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm");
+ public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt");
+ public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3");
+ public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3");
+ public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3");
+ public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3");
+ public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc");
+ public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh");
+ public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl");
+ public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse");
+ public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts");
+ public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt");
+ public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd");
+ public static final int TYPE_trex = Util.getIntegerCodeForString("trex");
+ public static final int TYPE_trun = Util.getIntegerCodeForString("trun");
+ public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx");
+ public static final int TYPE_moov = Util.getIntegerCodeForString("moov");
+ public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd");
+ public static final int TYPE_trak = Util.getIntegerCodeForString("trak");
+ public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia");
+ public static final int TYPE_minf = Util.getIntegerCodeForString("minf");
+ public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl");
+ public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC");
+ public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC");
+ public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
+ public static final int TYPE_moof = Util.getIntegerCodeForString("moof");
+ public static final int TYPE_traf = Util.getIntegerCodeForString("traf");
+ public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex");
+ public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd");
+ public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd");
+ public static final int TYPE_edts = Util.getIntegerCodeForString("edts");
+ public static final int TYPE_elst = Util.getIntegerCodeForString("elst");
+ public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd");
+ public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr");
+ public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd");
+ public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh");
+ public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf");
+ public static final int TYPE_schm = Util.getIntegerCodeForString("schm");
+ public static final int TYPE_schi = Util.getIntegerCodeForString("schi");
+ public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc");
+ public static final int TYPE_encv = Util.getIntegerCodeForString("encv");
+ public static final int TYPE_enca = Util.getIntegerCodeForString("enca");
+ public static final int TYPE_frma = Util.getIntegerCodeForString("frma");
+ public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz");
+ public static final int TYPE_saio = Util.getIntegerCodeForString("saio");
+ public static final int TYPE_sbgp = Util.getIntegerCodeForString("sbgp");
+ public static final int TYPE_sgpd = Util.getIntegerCodeForString("sgpd");
+ public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid");
+ public static final int TYPE_senc = Util.getIntegerCodeForString("senc");
+ public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp");
+ public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML");
+ public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd");
+ public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v");
+ public static final int TYPE_stts = Util.getIntegerCodeForString("stts");
+ public static final int TYPE_stss = Util.getIntegerCodeForString("stss");
+ public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts");
+ public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc");
+ public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz");
+ public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2");
+ public static final int TYPE_stco = Util.getIntegerCodeForString("stco");
+ public static final int TYPE_co64 = Util.getIntegerCodeForString("co64");
+ public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g");
+ public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt");
+ public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp");
+ public static final int TYPE_c608 = Util.getIntegerCodeForString("c608");
+ public static final int TYPE_samr = Util.getIntegerCodeForString("samr");
+ public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
+ public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
+ public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+ public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst");
+ public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
+ public static final int TYPE_name = Util.getIntegerCodeForString("name");
+ public static final int TYPE_data = Util.getIntegerCodeForString("data");
+ public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg");
+ public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d");
+ public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
+ public static final int TYPE_proj = Util.getIntegerCodeForString("proj");
+ public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08");
+ public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
+ public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
+ public static final int TYPE_camm = Util.getIntegerCodeForString("camm");
+ public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
+
+ public final int type;
+
+ public Atom(int type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return getAtomTypeString(type);
+ }
+
+ /**
+ * An MP4 atom that is a leaf.
+ */
+ /* package */ static final class LeafAtom extends Atom {
+
+ /**
+ * The atom data.
+ */
+ public final ParsableByteArray data;
+
+ /**
+ * @param type The type of the atom.
+ * @param data The atom data.
+ */
+ public LeafAtom(int type, ParsableByteArray data) {
+ super(type);
+ this.data = data;
+ }
+
+ }
+
+ /**
+ * An MP4 atom that has child atoms.
+ */
+ /* package */ static final class ContainerAtom extends Atom {
+
+ public final long endPosition;
+ public final List<LeafAtom> leafChildren;
+ public final List<ContainerAtom> containerChildren;
+
+ /**
+ * @param type The type of the atom.
+ * @param endPosition The position of the first byte after the end of the atom.
+ */
+ public ContainerAtom(int type, long endPosition) {
+ super(type);
+ this.endPosition = endPosition;
+ leafChildren = new ArrayList<>();
+ containerChildren = new ArrayList<>();
+ }
+
+ /**
+ * Adds a child leaf to this container.
+ *
+ * @param atom The child to add.
+ */
+ public void add(LeafAtom atom) {
+ leafChildren.add(atom);
+ }
+
+ /**
+ * Adds a child container to this container.
+ *
+ * @param atom The child to add.
+ */
+ public void add(ContainerAtom atom) {
+ containerChildren.add(atom);
+ }
+
+ /**
+ * Returns the child leaf of the given type.
+ * <p>
+ * If no child exists with the given type then null is returned. If multiple children exist with
+ * the given type then the first one to have been added is returned.
+ *
+ * @param type The leaf type.
+ * @return The child leaf of the given type, or null if no such child exists.
+ */
+ public LeafAtom getLeafAtomOfType(int type) {
+ int childrenSize = leafChildren.size();
+ for (int i = 0; i < childrenSize; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == type) {
+ return atom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the child container of the given type.
+ * <p>
+ * If no child exists with the given type then null is returned. If multiple children exist with
+ * the given type then the first one to have been added is returned.
+ *
+ * @param type The container type.
+ * @return The child container of the given type, or null if no such child exists.
+ */
+ public ContainerAtom getContainerAtomOfType(int type) {
+ int childrenSize = containerChildren.size();
+ for (int i = 0; i < childrenSize; i++) {
+ ContainerAtom atom = containerChildren.get(i);
+ if (atom.type == type) {
+ return atom;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the total number of leaf/container children of this atom with the given type.
+ *
+ * @param type The type of child atoms to count.
+ * @return The total number of leaf/container children of this atom with the given type.
+ */
+ public int getChildAtomOfTypeCount(int type) {
+ int count = 0;
+ int size = leafChildren.size();
+ for (int i = 0; i < size; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == type) {
+ count++;
+ }
+ }
+ size = containerChildren.size();
+ for (int i = 0; i < size; i++) {
+ ContainerAtom atom = containerChildren.get(i);
+ if (atom.type == type) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public String toString() {
+ return getAtomTypeString(type)
+ + " leaves: " + Arrays.toString(leafChildren.toArray())
+ + " containers: " + Arrays.toString(containerChildren.toArray());
+ }
+
+ }
+
+ /**
+ * Parses the version number out of the additional integer component of a full atom.
+ */
+ public static int parseFullAtomVersion(int fullAtomInt) {
+ return 0x000000FF & (fullAtomInt >> 24);
+ }
+
+ /**
+ * Parses the atom flags out of the additional integer component of a full atom.
+ */
+ public static int parseFullAtomFlags(int fullAtomInt) {
+ return 0x00FFFFFF & fullAtomInt;
+ }
+
+ /**
+ * Converts a numeric atom type to the corresponding four character string.
+ *
+ * @param type The numeric atom type.
+ * @return The corresponding four character string.
+ */
+ public static String getAtomTypeString(int type) {
+ return "" + (char) ((type >> 24) & 0xFF)
+ + (char) ((type >> 16) & 0xFF)
+ + (char) ((type >> 8) & 0xFF)
+ + (char) (type & 0xFF);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -0,0 +1,1326 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.audio.Ac3Util;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.AvcConfig;
+import com.google.android.exoplayer2.video.HevcConfig;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility methods for parsing MP4 format atom payloads according to ISO 14496-12.
+ */
+/* package */ final class AtomParsers {
+
+ private static final String TAG = "AtomParsers";
+
+ private static final int TYPE_vide = Util.getIntegerCodeForString("vide");
+ private static final int TYPE_soun = Util.getIntegerCodeForString("soun");
+ private static final int TYPE_text = Util.getIntegerCodeForString("text");
+ private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
+ private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
+ private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
+ private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc");
+ private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+
+ /**
+ * Parses a trak atom (defined in 14496-12).
+ *
+ * @param trak Atom to decode.
+ * @param mvhd Movie header atom, used to get the timescale.
+ * @param duration The duration in units of the timescale declared in the mvhd atom, or
+ * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
+ */
+ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration,
+ DrmInitData drmInitData, boolean isQuickTime) throws ParserException {
+ Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
+ int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
+ if (trackType == C.TRACK_TYPE_UNKNOWN) {
+ return null;
+ }
+
+ TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
+ if (duration == C.TIME_UNSET) {
+ duration = tkhdData.duration;
+ }
+ long movieTimescale = parseMvhd(mvhd.data);
+ long durationUs;
+ if (duration == C.TIME_UNSET) {
+ durationUs = C.TIME_UNSET;
+ } else {
+ durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale);
+ }
+ Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
+ .getContainerAtomOfType(Atom.TYPE_stbl);
+
+ Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
+ StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id,
+ tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime);
+ Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
+ return stsdData.format == null ? null
+ : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
+ stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
+ stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second);
+ }
+
+ /**
+ * Parses an stbl atom (defined in 14496-12).
+ *
+ * @param track Track to which this sample table corresponds.
+ * @param stblAtom stbl (sample table) atom to decode.
+ * @param gaplessInfoHolder Holder to populate with gapless playback information.
+ * @return Sample table described by the stbl atom.
+ * @throws ParserException If the resulting sample sequence does not contain a sync sample.
+ */
+ public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom,
+ GaplessInfoHolder gaplessInfoHolder) throws ParserException {
+ SampleSizeBox sampleSizeBox;
+ Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
+ if (stszAtom != null) {
+ sampleSizeBox = new StszSampleSizeBox(stszAtom);
+ } else {
+ Atom.LeafAtom stz2Atom = stblAtom.getLeafAtomOfType(Atom.TYPE_stz2);
+ if (stz2Atom == null) {
+ throw new ParserException("Track has no sample table size information");
+ }
+ sampleSizeBox = new Stz2SampleSizeBox(stz2Atom);
+ }
+
+ int sampleCount = sampleSizeBox.getSampleCount();
+ if (sampleCount == 0) {
+ return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]);
+ }
+
+ // Entries are byte offsets of chunks.
+ boolean chunkOffsetsAreLongs = false;
+ Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco);
+ if (chunkOffsetsAtom == null) {
+ chunkOffsetsAreLongs = true;
+ chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64);
+ }
+ ParsableByteArray chunkOffsets = chunkOffsetsAtom.data;
+ // Entries are (chunk number, number of samples per chunk, sample description index).
+ ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data;
+ // Entries are (number of samples, timestamp delta between those samples).
+ ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data;
+ // Entries are the indices of samples that are synchronization samples.
+ Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss);
+ ParsableByteArray stss = stssAtom != null ? stssAtom.data : null;
+ // Entries are (number of samples, timestamp offset).
+ Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts);
+ ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
+
+ // Prepare to read chunk information.
+ ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs);
+
+ // Prepare to read sample timestamps.
+ stts.setPosition(Atom.FULL_HEADER_SIZE);
+ int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
+ int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
+ int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
+
+ // Prepare to read sample timestamp offsets, if ctts is present.
+ int remainingSamplesAtTimestampOffset = 0;
+ int remainingTimestampOffsetChanges = 0;
+ int timestampOffset = 0;
+ if (ctts != null) {
+ ctts.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt();
+ }
+
+ int nextSynchronizationSampleIndex = C.INDEX_UNSET;
+ int remainingSynchronizationSamples = 0;
+ if (stss != null) {
+ stss.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingSynchronizationSamples = stss.readUnsignedIntToInt();
+ if (remainingSynchronizationSamples > 0) {
+ nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
+ } else {
+ // Ignore empty stss boxes, which causes all samples to be treated as sync samples.
+ stss = null;
+ }
+ }
+
+ // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio.
+ boolean isRechunkable = sampleSizeBox.isFixedSampleSize()
+ && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
+ && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0
+ && remainingSynchronizationSamples == 0;
+
+ long[] offsets;
+ int[] sizes;
+ int maximumSize = 0;
+ long[] timestamps;
+ int[] flags;
+ long timestampTimeUnits = 0;
+
+ if (!isRechunkable) {
+ offsets = new long[sampleCount];
+ sizes = new int[sampleCount];
+ timestamps = new long[sampleCount];
+ flags = new int[sampleCount];
+ long offset = 0;
+ int remainingSamplesInChunk = 0;
+
+ for (int i = 0; i < sampleCount; i++) {
+ // Advance to the next chunk if necessary.
+ while (remainingSamplesInChunk == 0) {
+ Assertions.checkState(chunkIterator.moveNext());
+ offset = chunkIterator.offset;
+ remainingSamplesInChunk = chunkIterator.numSamples;
+ }
+
+ // Add on the timestamp offset if ctts is present.
+ if (ctts != null) {
+ while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
+ remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
+ // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers
+ // in version 0 ctts boxes, however some streams violate the spec and use signed
+ // integers instead. It's safe to always decode sample offsets as signed integers here,
+ // because unsigned integers will still be parsed correctly (unless their top bit is
+ // set, which is never true in practice because sample offsets are always small).
+ timestampOffset = ctts.readInt();
+ remainingTimestampOffsetChanges--;
+ }
+ remainingSamplesAtTimestampOffset--;
+ }
+
+ offsets[i] = offset;
+ sizes[i] = sampleSizeBox.readNextSampleSize();
+ if (sizes[i] > maximumSize) {
+ maximumSize = sizes[i];
+ }
+ timestamps[i] = timestampTimeUnits + timestampOffset;
+
+ // All samples are synchronization samples if the stss is not present.
+ flags[i] = stss == null ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ if (i == nextSynchronizationSampleIndex) {
+ flags[i] = C.BUFFER_FLAG_KEY_FRAME;
+ remainingSynchronizationSamples--;
+ if (remainingSynchronizationSamples > 0) {
+ nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
+ }
+ }
+
+ // Add on the duration of this sample.
+ timestampTimeUnits += timestampDeltaInTimeUnits;
+ remainingSamplesAtTimestampDelta--;
+ if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) {
+ remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
+ timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
+ remainingTimestampDeltaChanges--;
+ }
+
+ offset += sizes[i];
+ remainingSamplesInChunk--;
+ }
+
+ Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
+ // Remove trailing ctts entries with 0-valued sample counts.
+ while (remainingTimestampOffsetChanges > 0) {
+ Assertions.checkArgument(ctts.readUnsignedIntToInt() == 0);
+ ctts.readInt(); // Ignore offset.
+ remainingTimestampOffsetChanges--;
+ }
+
+ // If the stbl's child boxes are not consistent the container is malformed, but the stream may
+ // still be playable.
+ if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0
+ || remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) {
+ Log.w(TAG, "Inconsistent stbl box for track " + track.id
+ + ": remainingSynchronizationSamples " + remainingSynchronizationSamples
+ + ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta
+ + ", remainingSamplesInChunk " + remainingSamplesInChunk
+ + ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges);
+ }
+ } else {
+ long[] chunkOffsetsBytes = new long[chunkIterator.length];
+ int[] chunkSampleCounts = new int[chunkIterator.length];
+ while (chunkIterator.moveNext()) {
+ chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
+ chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
+ }
+ int fixedSampleSize = sampleSizeBox.readNextSampleSize();
+ FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
+ fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
+ offsets = rechunkedResults.offsets;
+ sizes = rechunkedResults.sizes;
+ maximumSize = rechunkedResults.maximumSize;
+ timestamps = rechunkedResults.timestamps;
+ flags = rechunkedResults.flags;
+ }
+
+ if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
+ // There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
+ // This implementation does not support applying both gapless metadata and an edit list.
+ Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ }
+
+ // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
+ // sync sample after reordering are not supported. Partial audio sample truncation is only
+ // supported in edit lists with one edit that removes less than one sample from the start/end of
+ // the track, for gapless audio playback. This implementation handles simple discarding/delaying
+ // of samples. The extractor may place further restrictions on what edited streams are playable.
+
+ if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO
+ && timestamps.length >= 2) {
+ // Handle the edit by setting gapless playback metadata, if possible. This implementation
+ // assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end
+ // points of the edit must lie within the first/last samples respectively.
+ long editStartTime = track.editListMediaTimes[0];
+ long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
+ track.timescale, track.movieTimescale);
+ long lastSampleEndTime = timestampTimeUnits;
+ if (timestamps[0] <= editStartTime && editStartTime < timestamps[1]
+ && timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) {
+ long paddingTimeUnits = lastSampleEndTime - editEndTime;
+ long encoderDelay = Util.scaleLargeTimestamp(editStartTime - timestamps[0],
+ track.format.sampleRate, track.timescale);
+ long encoderPadding = Util.scaleLargeTimestamp(paddingTimeUnits,
+ track.format.sampleRate, track.timescale);
+ if ((encoderDelay != 0 || encoderPadding != 0) && encoderDelay <= Integer.MAX_VALUE
+ && encoderPadding <= Integer.MAX_VALUE) {
+ gaplessInfoHolder.encoderDelay = (int) encoderDelay;
+ gaplessInfoHolder.encoderPadding = (int) encoderPadding;
+ Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ }
+ }
+ }
+
+ if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) {
+ // The current version of the spec leaves handling of an edit with zero segment_duration in
+ // unfragmented files open to interpretation. We handle this as a special case and include all
+ // samples in the edit.
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0],
+ C.MICROS_PER_SECOND, track.timescale);
+ }
+ return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+ }
+
+ // Omit any sample at the end point of an edit for audio tracks.
+ boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
+
+ // Count the number of samples after applying edits.
+ int editedSampleCount = 0;
+ int nextSampleIndex = 0;
+ boolean copyMetadata = false;
+ for (int i = 0; i < track.editListDurations.length; i++) {
+ long mediaTime = track.editListMediaTimes[i];
+ if (mediaTime != -1) {
+ long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
+ track.movieTimescale);
+ int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
+ int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample,
+ false);
+ editedSampleCount += endIndex - startIndex;
+ copyMetadata |= nextSampleIndex != startIndex;
+ nextSampleIndex = endIndex;
+ }
+ }
+ copyMetadata |= editedSampleCount != sampleCount;
+
+ // Calculate edited sample timestamps and update the corresponding metadata arrays.
+ long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets;
+ int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes;
+ int editedMaximumSize = copyMetadata ? 0 : maximumSize;
+ int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags;
+ long[] editedTimestamps = new long[editedSampleCount];
+ long pts = 0;
+ int sampleIndex = 0;
+ for (int i = 0; i < track.editListDurations.length; i++) {
+ long mediaTime = track.editListMediaTimes[i];
+ long duration = track.editListDurations[i];
+ if (mediaTime != -1) {
+ long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
+ track.movieTimescale);
+ int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
+ int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
+ if (copyMetadata) {
+ int count = endIndex - startIndex;
+ System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
+ System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
+ System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
+ }
+ for (int j = startIndex; j < endIndex; j++) {
+ long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
+ long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime,
+ C.MICROS_PER_SECOND, track.timescale);
+ editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
+ if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
+ editedMaximumSize = sizes[j];
+ }
+ sampleIndex++;
+ }
+ }
+ pts += duration;
+ }
+
+ boolean hasSyncSample = false;
+ for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
+ hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
+ }
+ if (!hasSyncSample) {
+ throw new ParserException("The edited sample sequence does not contain a sync sample.");
+ }
+
+ return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps,
+ editedFlags);
+ }
+
+ /**
+ * Parses a udta atom.
+ *
+ * @param udtaAtom The udta (user data) atom to decode.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return Parsed metadata, or null.
+ */
+ public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
+ if (isQuickTime) {
+ // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
+ // decode one.
+ return null;
+ }
+ ParsableByteArray udtaData = udtaAtom.data;
+ udtaData.setPosition(Atom.HEADER_SIZE);
+ while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) {
+ int atomPosition = udtaData.getPosition();
+ int atomSize = udtaData.readInt();
+ int atomType = udtaData.readInt();
+ if (atomType == Atom.TYPE_meta) {
+ udtaData.setPosition(atomPosition);
+ return parseMetaAtom(udtaData, atomPosition + atomSize);
+ }
+ udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
+ }
+ return null;
+ }
+
+ private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) {
+ meta.skipBytes(Atom.FULL_HEADER_SIZE);
+ while (meta.getPosition() < limit) {
+ int atomPosition = meta.getPosition();
+ int atomSize = meta.readInt();
+ int atomType = meta.readInt();
+ if (atomType == Atom.TYPE_ilst) {
+ meta.setPosition(atomPosition);
+ return parseIlst(meta, atomPosition + atomSize);
+ }
+ meta.skipBytes(atomSize - Atom.HEADER_SIZE);
+ }
+ return null;
+ }
+
+ private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
+ ilst.skipBytes(Atom.HEADER_SIZE);
+ ArrayList<Metadata.Entry> entries = new ArrayList<>();
+ while (ilst.getPosition() < limit) {
+ Metadata.Entry entry = MetadataUtil.parseIlstElement(ilst);
+ if (entry != null) {
+ entries.add(entry);
+ }
+ }
+ return entries.isEmpty() ? null : new Metadata(entries);
+ }
+
+ /**
+ * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie.
+ *
+ * @param mvhd Contents of the mvhd atom to be parsed.
+ * @return Timescale for the movie.
+ */
+ private static long parseMvhd(ParsableByteArray mvhd) {
+ mvhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mvhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ mvhd.skipBytes(version == 0 ? 8 : 16);
+ return mvhd.readUnsignedInt();
+ }
+
+ /**
+ * Parses a tkhd atom (defined in 14496-12).
+ *
+ * @return An object containing the parsed data.
+ */
+ private static TkhdData parseTkhd(ParsableByteArray tkhd) {
+ tkhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tkhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+
+ tkhd.skipBytes(version == 0 ? 8 : 16);
+ int trackId = tkhd.readInt();
+
+ tkhd.skipBytes(4);
+ boolean durationUnknown = true;
+ int durationPosition = tkhd.getPosition();
+ int durationByteCount = version == 0 ? 4 : 8;
+ for (int i = 0; i < durationByteCount; i++) {
+ if (tkhd.data[durationPosition + i] != -1) {
+ durationUnknown = false;
+ break;
+ }
+ }
+ long duration;
+ if (durationUnknown) {
+ tkhd.skipBytes(durationByteCount);
+ duration = C.TIME_UNSET;
+ } else {
+ duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();
+ if (duration == 0) {
+ // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media
+ // samples are in fragments). Treat as unknown.
+ duration = C.TIME_UNSET;
+ }
+ }
+
+ tkhd.skipBytes(16);
+ int a00 = tkhd.readInt();
+ int a01 = tkhd.readInt();
+ tkhd.skipBytes(4);
+ int a10 = tkhd.readInt();
+ int a11 = tkhd.readInt();
+
+ int rotationDegrees;
+ int fixedOne = 65536;
+ if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) {
+ rotationDegrees = 90;
+ } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) {
+ rotationDegrees = 270;
+ } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) {
+ rotationDegrees = 180;
+ } else {
+ // Only 0, 90, 180 and 270 are supported. Treat anything else as 0.
+ rotationDegrees = 0;
+ }
+
+ return new TkhdData(trackId, duration, rotationDegrees);
+ }
+
+ /**
+ * Parses an hdlr atom.
+ *
+ * @param hdlr The hdlr atom to decode.
+ * @return The track type.
+ */
+ private static int parseHdlr(ParsableByteArray hdlr) {
+ hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
+ int trackType = hdlr.readInt();
+ if (trackType == TYPE_soun) {
+ return C.TRACK_TYPE_AUDIO;
+ } else if (trackType == TYPE_vide) {
+ return C.TRACK_TYPE_VIDEO;
+ } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt
+ || trackType == TYPE_clcp) {
+ return C.TRACK_TYPE_TEXT;
+ } else if (trackType == TYPE_meta) {
+ return C.TRACK_TYPE_METADATA;
+ } else {
+ return C.TRACK_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Parses an mdhd atom (defined in 14496-12).
+ *
+ * @param mdhd The mdhd atom to decode.
+ * @return A pair consisting of the media timescale defined as the number of time units that pass
+ * in one second, and the language code.
+ */
+ private static Pair<Long, String> parseMdhd(ParsableByteArray mdhd) {
+ mdhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mdhd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ mdhd.skipBytes(version == 0 ? 8 : 16);
+ long timescale = mdhd.readUnsignedInt();
+ mdhd.skipBytes(version == 0 ? 4 : 8);
+ int languageCode = mdhd.readUnsignedShort();
+ String language = "" + (char) (((languageCode >> 10) & 0x1F) + 0x60)
+ + (char) (((languageCode >> 5) & 0x1F) + 0x60)
+ + (char) (((languageCode) & 0x1F) + 0x60);
+ return Pair.create(timescale, language);
+ }
+
+ /**
+ * Parses a stsd atom (defined in 14496-12).
+ *
+ * @param stsd The stsd atom to decode.
+ * @param trackId The track's identifier in its container.
+ * @param rotationDegrees The rotation of the track in degrees.
+ * @param language The language of the track.
+ * @param drmInitData {@link DrmInitData} to be included in the format.
+ * @param isQuickTime True for QuickTime media. False otherwise.
+ * @return An object containing the parsed data.
+ */
+ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotationDegrees,
+ String language, DrmInitData drmInitData, boolean isQuickTime) throws ParserException {
+ stsd.setPosition(Atom.FULL_HEADER_SIZE);
+ int numberOfEntries = stsd.readInt();
+ StsdData out = new StsdData(numberOfEntries);
+ for (int i = 0; i < numberOfEntries; i++) {
+ int childStartPosition = stsd.getPosition();
+ int childAtomSize = stsd.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = stsd.readInt();
+ if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
+ || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v
+ || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1
+ || childAtomType == Atom.TYPE_s263 || childAtomType == Atom.TYPE_vp08
+ || childAtomType == Atom.TYPE_vp09) {
+ parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+ rotationDegrees, drmInitData, out, i);
+ } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
+ || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3
+ || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse
+ || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl
+ || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb
+ || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt
+ || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) {
+ parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+ language, isQuickTime, drmInitData, out, i);
+ } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g
+ || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp
+ || childAtomType == Atom.TYPE_c608) {
+ parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+ language, drmInitData, out);
+ } else if (childAtomType == Atom.TYPE_camm) {
+ out.format = Format.createSampleFormat(Integer.toString(trackId),
+ MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData);
+ }
+ stsd.setPosition(childStartPosition + childAtomSize);
+ }
+ return out;
+ }
+
+ private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out)
+ throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ // Default values.
+ List<byte[]> initializationData = null;
+ long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE;
+
+ String mimeType;
+ if (atomType == Atom.TYPE_TTML) {
+ mimeType = MimeTypes.APPLICATION_TTML;
+ } else if (atomType == Atom.TYPE_tx3g) {
+ mimeType = MimeTypes.APPLICATION_TX3G;
+ int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8;
+ byte[] sampleDescriptionData = new byte[sampleDescriptionLength];
+ parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength);
+ initializationData = Collections.singletonList(sampleDescriptionData);
+ } else if (atomType == Atom.TYPE_wvtt) {
+ mimeType = MimeTypes.APPLICATION_MP4VTT;
+ } else if (atomType == Atom.TYPE_stpp) {
+ mimeType = MimeTypes.APPLICATION_TTML;
+ subsampleOffsetUs = 0; // Subsample timing is absolute.
+ } else if (atomType == Atom.TYPE_c608) {
+ // Defined by the QuickTime File Format specification.
+ mimeType = MimeTypes.APPLICATION_MP4CEA608;
+ out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
+ } else {
+ // Never happens.
+ throw new IllegalStateException();
+ }
+
+ out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs,
+ initializationData);
+ }
+
+ private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out,
+ int entryIndex) throws ParserException {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ parent.skipBytes(16);
+ int width = parent.readUnsignedShort();
+ int height = parent.readUnsignedShort();
+ boolean pixelWidthHeightRatioFromPasp = false;
+ float pixelWidthHeightRatio = 1;
+ parent.skipBytes(50);
+
+ int childPosition = parent.getPosition();
+ if (atomType == Atom.TYPE_encv) {
+ atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+ parent.setPosition(childPosition);
+ }
+
+ List<byte[]> initializationData = null;
+ String mimeType = null;
+ byte[] projectionData = null;
+ @C.StereoMode
+ int stereoMode = Format.NO_VALUE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childStartPosition = parent.getPosition();
+ int childAtomSize = parent.readInt();
+ if (childAtomSize == 0 && parent.getPosition() - position == size) {
+ // Handle optional terminating four zero bytes in MOV files.
+ break;
+ }
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_avcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H264;
+ parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
+ AvcConfig avcConfig = AvcConfig.parse(parent);
+ initializationData = avcConfig.initializationData;
+ out.nalUnitLengthFieldLength = avcConfig.nalUnitLengthFieldLength;
+ if (!pixelWidthHeightRatioFromPasp) {
+ pixelWidthHeightRatio = avcConfig.pixelWidthAspectRatio;
+ }
+ } else if (childAtomType == Atom.TYPE_hvcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H265;
+ parent.setPosition(childStartPosition + Atom.HEADER_SIZE);
+ HevcConfig hevcConfig = HevcConfig.parse(parent);
+ initializationData = hevcConfig.initializationData;
+ out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength;
+ } else if (childAtomType == Atom.TYPE_vpcC) {
+ Assertions.checkState(mimeType == null);
+ mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9;
+ } else if (childAtomType == Atom.TYPE_d263) {
+ Assertions.checkState(mimeType == null);
+ mimeType = MimeTypes.VIDEO_H263;
+ } else if (childAtomType == Atom.TYPE_esds) {
+ Assertions.checkState(mimeType == null);
+ Pair<String, byte[]> mimeTypeAndInitializationData =
+ parseEsdsFromParent(parent, childStartPosition);
+ mimeType = mimeTypeAndInitializationData.first;
+ initializationData = Collections.singletonList(mimeTypeAndInitializationData.second);
+ } else if (childAtomType == Atom.TYPE_pasp) {
+ pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);
+ pixelWidthHeightRatioFromPasp = true;
+ } else if (childAtomType == Atom.TYPE_sv3d) {
+ projectionData = parseProjFromParent(parent, childStartPosition, childAtomSize);
+ } else if (childAtomType == Atom.TYPE_st3d) {
+ int version = parent.readUnsignedByte();
+ parent.skipBytes(3); // Flags.
+ if (version == 0) {
+ int layout = parent.readUnsignedByte();
+ switch (layout) {
+ case 0:
+ stereoMode = C.STEREO_MODE_MONO;
+ break;
+ case 1:
+ stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ break;
+ case 2:
+ stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ break;
+ case 3:
+ stereoMode = C.STEREO_MODE_STEREO_MESH;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ childPosition += childAtomSize;
+ }
+
+ // If the media type was not recognized, ignore the track.
+ if (mimeType == null) {
+ return;
+ }
+
+ out.format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, initializationData,
+ rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, null, drmInitData);
+ }
+
+ /**
+ * Parses the edts atom (defined in 14496-12 subsection 8.6.5).
+ *
+ * @param edtsAtom edts (edit box) atom to decode.
+ * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are
+ * not present.
+ */
+ private static Pair<long[], long[]> parseEdts(Atom.ContainerAtom edtsAtom) {
+ Atom.LeafAtom elst;
+ if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) {
+ return Pair.create(null, null);
+ }
+ ParsableByteArray elstData = elst.data;
+ elstData.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = elstData.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ int entryCount = elstData.readUnsignedIntToInt();
+ long[] editListDurations = new long[entryCount];
+ long[] editListMediaTimes = new long[entryCount];
+ for (int i = 0; i < entryCount; i++) {
+ editListDurations[i] =
+ version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt();
+ editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt();
+ int mediaRateInteger = elstData.readShort();
+ if (mediaRateInteger != 1) {
+ // The extractor does not handle dwell edits (mediaRateInteger == 0).
+ throw new IllegalArgumentException("Unsupported media rate.");
+ }
+ elstData.skipBytes(2);
+ }
+ return Pair.create(editListDurations, editListMediaTimes);
+ }
+
+ private static float parsePaspFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + Atom.HEADER_SIZE);
+ int hSpacing = parent.readUnsignedIntToInt();
+ int vSpacing = parent.readUnsignedIntToInt();
+ return (float) hSpacing / vSpacing;
+ }
+
+ private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
+ int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,
+ StsdData out, int entryIndex) {
+ parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
+
+ int quickTimeSoundDescriptionVersion = 0;
+ if (isQuickTime) {
+ quickTimeSoundDescriptionVersion = parent.readUnsignedShort();
+ parent.skipBytes(6);
+ } else {
+ parent.skipBytes(8);
+ }
+
+ int channelCount;
+ int sampleRate;
+
+ if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
+ channelCount = parent.readUnsignedShort();
+ parent.skipBytes(6); // sampleSize, compressionId, packetSize.
+ sampleRate = parent.readUnsignedFixedPoint1616();
+
+ if (quickTimeSoundDescriptionVersion == 1) {
+ parent.skipBytes(16);
+ }
+ } else if (quickTimeSoundDescriptionVersion == 2) {
+ parent.skipBytes(16); // always[3,16,Minus2,0,65536], sizeOfStructOnly
+
+ sampleRate = (int) Math.round(parent.readDouble());
+ channelCount = parent.readUnsignedIntToInt();
+
+ // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket,
+ // constLPCMFramesPerAudioPacket.
+ parent.skipBytes(20);
+ } else {
+ // Unsupported version.
+ return;
+ }
+
+ int childPosition = parent.getPosition();
+ if (atomType == Atom.TYPE_enca) {
+ atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+ parent.setPosition(childPosition);
+ }
+
+ // If the atom type determines a MIME type, set it immediately.
+ String mimeType = null;
+ if (atomType == Atom.TYPE_ac_3) {
+ mimeType = MimeTypes.AUDIO_AC3;
+ } else if (atomType == Atom.TYPE_ec_3) {
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ } else if (atomType == Atom.TYPE_dtsc) {
+ mimeType = MimeTypes.AUDIO_DTS;
+ } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) {
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ } else if (atomType == Atom.TYPE_dtse) {
+ mimeType = MimeTypes.AUDIO_DTS_EXPRESS;
+ } else if (atomType == Atom.TYPE_samr) {
+ mimeType = MimeTypes.AUDIO_AMR_NB;
+ } else if (atomType == Atom.TYPE_sawb) {
+ mimeType = MimeTypes.AUDIO_AMR_WB;
+ } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
+ mimeType = MimeTypes.AUDIO_RAW;
+ } else if (atomType == Atom.TYPE__mp3) {
+ mimeType = MimeTypes.AUDIO_MPEG;
+ } else if (atomType == Atom.TYPE_alac) {
+ mimeType = MimeTypes.AUDIO_ALAC;
+ }
+
+ byte[] initializationData = null;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) {
+ int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition
+ : findEsdsPosition(parent, childPosition, childAtomSize);
+ if (esdsAtomPosition != C.POSITION_UNSET) {
+ Pair<String, byte[]> mimeTypeAndInitializationData =
+ parseEsdsFromParent(parent, esdsAtomPosition);
+ mimeType = mimeTypeAndInitializationData.first;
+ initializationData = mimeTypeAndInitializationData.second;
+ if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
+ // TODO: Do we really need to do this? See [Internal: b/10903778]
+ // Update sampleRate and channelCount from the AudioSpecificConfig initialization data.
+ Pair<Integer, Integer> audioSpecificConfig =
+ CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData);
+ sampleRate = audioSpecificConfig.first;
+ channelCount = audioSpecificConfig.second;
+ }
+ }
+ } else if (childAtomType == Atom.TYPE_dac3) {
+ parent.setPosition(Atom.HEADER_SIZE + childPosition);
+ out.format = Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), language,
+ drmInitData);
+ } else if (childAtomType == Atom.TYPE_dec3) {
+ parent.setPosition(Atom.HEADER_SIZE + childPosition);
+ out.format = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), language,
+ drmInitData);
+ } else if (childAtomType == Atom.TYPE_ddts) {
+ out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
+ language);
+ } else if (childAtomType == Atom.TYPE_alac) {
+ initializationData = new byte[childAtomSize];
+ parent.setPosition(childPosition);
+ parent.readBytes(initializationData, 0, childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+
+ if (out.format == null && mimeType != null) {
+ // TODO: Determine the correct PCM encoding.
+ @C.PcmEncoding int pcmEncoding =
+ MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE;
+ out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
+ initializationData == null ? null : Collections.singletonList(initializationData),
+ drmInitData, 0, language);
+ }
+ }
+
+ /**
+ * Returns the position of the esds box within a parent, or {@link C#POSITION_UNSET} if no esds
+ * box is found
+ */
+ private static int findEsdsPosition(ParsableByteArray parent, int position, int size) {
+ int childAtomPosition = parent.getPosition();
+ while (childAtomPosition - position < size) {
+ parent.setPosition(childAtomPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childType = parent.readInt();
+ if (childType == Atom.TYPE_esds) {
+ return childAtomPosition;
+ }
+ childAtomPosition += childAtomSize;
+ }
+ return C.POSITION_UNSET;
+ }
+
+ /**
+ * Returns codec-specific initialization data contained in an esds box.
+ */
+ private static Pair<String, byte[]> parseEsdsFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + Atom.HEADER_SIZE + 4);
+ // Start of the ES_Descriptor (defined in 14496-1)
+ parent.skipBytes(1); // ES_Descriptor tag
+ parseExpandableClassSize(parent);
+ parent.skipBytes(2); // ES_ID
+
+ int flags = parent.readUnsignedByte();
+ if ((flags & 0x80 /* streamDependenceFlag */) != 0) {
+ parent.skipBytes(2);
+ }
+ if ((flags & 0x40 /* URL_Flag */) != 0) {
+ parent.skipBytes(parent.readUnsignedShort());
+ }
+ if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
+ parent.skipBytes(2);
+ }
+
+ // Start of the DecoderConfigDescriptor (defined in 14496-1)
+ parent.skipBytes(1); // DecoderConfigDescriptor tag
+ parseExpandableClassSize(parent);
+
+ // Set the MIME type based on the object type indication (14496-1 table 5).
+ int objectTypeIndication = parent.readUnsignedByte();
+ String mimeType;
+ switch (objectTypeIndication) {
+ case 0x6B:
+ mimeType = MimeTypes.AUDIO_MPEG;
+ return Pair.create(mimeType, null);
+ case 0x20:
+ mimeType = MimeTypes.VIDEO_MP4V;
+ break;
+ case 0x21:
+ mimeType = MimeTypes.VIDEO_H264;
+ break;
+ case 0x23:
+ mimeType = MimeTypes.VIDEO_H265;
+ break;
+ case 0x40:
+ case 0x66:
+ case 0x67:
+ case 0x68:
+ mimeType = MimeTypes.AUDIO_AAC;
+ break;
+ case 0xA5:
+ mimeType = MimeTypes.AUDIO_AC3;
+ break;
+ case 0xA6:
+ mimeType = MimeTypes.AUDIO_E_AC3;
+ break;
+ case 0xA9:
+ case 0xAC:
+ mimeType = MimeTypes.AUDIO_DTS;
+ return Pair.create(mimeType, null);
+ case 0xAA:
+ case 0xAB:
+ mimeType = MimeTypes.AUDIO_DTS_HD;
+ return Pair.create(mimeType, null);
+ default:
+ mimeType = null;
+ break;
+ }
+
+ parent.skipBytes(12);
+
+ // Start of the AudioSpecificConfig.
+ parent.skipBytes(1); // AudioSpecificConfig tag
+ int initializationDataSize = parseExpandableClassSize(parent);
+ byte[] initializationData = new byte[initializationDataSize];
+ parent.readBytes(initializationData, 0, initializationDataSize);
+ return Pair.create(mimeType, initializationData);
+ }
+
+ /**
+ * Parses encryption data from an audio/video sample entry, populating {@code out} and returning
+ * the unencrypted atom type, or 0 if no common encryption sinf atom was present.
+ */
+ private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position,
+ int size, StsdData out, int entryIndex) {
+ int childPosition = parent.getPosition();
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_sinf) {
+ Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition,
+ childAtomSize);
+ if (result != null) {
+ out.trackEncryptionBoxes[entryIndex] = result.second;
+ return result.first;
+ }
+ }
+ childPosition += childAtomSize;
+ }
+ // This enca/encv box does not have a data format so return an invalid atom type.
+ return 0;
+ }
+
+ private static Pair<Integer, TrackEncryptionBox> parseSinfFromParent(ParsableByteArray parent,
+ int position, int size) {
+ int childPosition = position + Atom.HEADER_SIZE;
+
+ boolean isCencScheme = false;
+ TrackEncryptionBox trackEncryptionBox = null;
+ Integer dataFormat = null;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_frma) {
+ dataFormat = parent.readInt();
+ } else if (childAtomType == Atom.TYPE_schm) {
+ parent.skipBytes(4);
+ isCencScheme = parent.readInt() == TYPE_cenc;
+ } else if (childAtomType == Atom.TYPE_schi) {
+ trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+
+ if (isCencScheme) {
+ Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
+ Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory");
+ return Pair.create(dataFormat, trackEncryptionBox);
+ } else {
+ return null;
+ }
+ }
+
+ private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
+ int size) {
+ int childPosition = position + Atom.HEADER_SIZE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_tenc) {
+ parent.skipBytes(6);
+ boolean defaultIsEncrypted = parent.readUnsignedByte() == 1;
+ int defaultInitVectorSize = parent.readUnsignedByte();
+ byte[] defaultKeyId = new byte[16];
+ parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
+ return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId);
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /**
+ * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media
+ */
+ private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) {
+ int childPosition = position + Atom.HEADER_SIZE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_proj) {
+ return Arrays.copyOfRange(parent.data, childPosition, childPosition + childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ /**
+ * Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3.
+ */
+ private static int parseExpandableClassSize(ParsableByteArray data) {
+ int currentByte = data.readUnsignedByte();
+ int size = currentByte & 0x7F;
+ while ((currentByte & 0x80) == 0x80) {
+ currentByte = data.readUnsignedByte();
+ size = (size << 7) | (currentByte & 0x7F);
+ }
+ return size;
+ }
+
+ private AtomParsers() {
+ // Prevent instantiation.
+ }
+
+ private static final class ChunkIterator {
+
+ public final int length;
+
+ public int index;
+ public int numSamples;
+ public long offset;
+
+ private final boolean chunkOffsetsAreLongs;
+ private final ParsableByteArray chunkOffsets;
+ private final ParsableByteArray stsc;
+
+ private int nextSamplesPerChunkChangeIndex;
+ private int remainingSamplesPerChunkChanges;
+
+ public ChunkIterator(ParsableByteArray stsc, ParsableByteArray chunkOffsets,
+ boolean chunkOffsetsAreLongs) {
+ this.stsc = stsc;
+ this.chunkOffsets = chunkOffsets;
+ this.chunkOffsetsAreLongs = chunkOffsetsAreLongs;
+ chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);
+ length = chunkOffsets.readUnsignedIntToInt();
+ stsc.setPosition(Atom.FULL_HEADER_SIZE);
+ remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
+ Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
+ index = C.INDEX_UNSET;
+ }
+
+ public boolean moveNext() {
+ if (++index == length) {
+ return false;
+ }
+ offset = chunkOffsetsAreLongs ? chunkOffsets.readUnsignedLongToLong()
+ : chunkOffsets.readUnsignedInt();
+ if (index == nextSamplesPerChunkChangeIndex) {
+ numSamples = stsc.readUnsignedIntToInt();
+ stsc.skipBytes(4); // Skip sample_description_index
+ nextSamplesPerChunkChangeIndex = --remainingSamplesPerChunkChanges > 0
+ ? (stsc.readUnsignedIntToInt() - 1) : C.INDEX_UNSET;
+ }
+ return true;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from a tkhd atom.
+ */
+ private static final class TkhdData {
+
+ private final int id;
+ private final long duration;
+ private final int rotationDegrees;
+
+ public TkhdData(int id, long duration, int rotationDegrees) {
+ this.id = id;
+ this.duration = duration;
+ this.rotationDegrees = rotationDegrees;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from an stsd atom and its children.
+ */
+ private static final class StsdData {
+
+ public static final int STSD_HEADER_SIZE = 8;
+
+ public final TrackEncryptionBox[] trackEncryptionBoxes;
+
+ public Format format;
+ public int nalUnitLengthFieldLength;
+ @Track.Transformation
+ public int requiredSampleTransformation;
+
+ public StsdData(int numberOfEntries) {
+ trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
+ requiredSampleTransformation = Track.TRANSFORMATION_NONE;
+ }
+
+ }
+
+ /**
+ * A box containing sample sizes (e.g. stsz, stz2).
+ */
+ private interface SampleSizeBox {
+
+ /**
+ * Returns the number of samples.
+ */
+ int getSampleCount();
+
+ /**
+ * Returns the size for the next sample.
+ */
+ int readNextSampleSize();
+
+ /**
+ * Returns whether samples have a fixed size.
+ */
+ boolean isFixedSampleSize();
+
+ }
+
+ /**
+ * An stsz sample size box.
+ */
+ /* package */ static final class StszSampleSizeBox implements SampleSizeBox {
+
+ private final int fixedSampleSize;
+ private final int sampleCount;
+ private final ParsableByteArray data;
+
+ public StszSampleSizeBox(Atom.LeafAtom stszAtom) {
+ data = stszAtom.data;
+ data.setPosition(Atom.FULL_HEADER_SIZE);
+ fixedSampleSize = data.readUnsignedIntToInt();
+ sampleCount = data.readUnsignedIntToInt();
+ }
+
+ @Override
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ @Override
+ public int readNextSampleSize() {
+ return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize;
+ }
+
+ @Override
+ public boolean isFixedSampleSize() {
+ return fixedSampleSize != 0;
+ }
+
+ }
+
+ /**
+ * An stz2 sample size box.
+ */
+ /* package */ static final class Stz2SampleSizeBox implements SampleSizeBox {
+
+ private final ParsableByteArray data;
+ private final int sampleCount;
+ private final int fieldSize; // Can be 4, 8, or 16.
+
+ // Used only if fieldSize == 4.
+ private int sampleIndex;
+ private int currentByte;
+
+ public Stz2SampleSizeBox(Atom.LeafAtom stz2Atom) {
+ data = stz2Atom.data;
+ data.setPosition(Atom.FULL_HEADER_SIZE);
+ fieldSize = data.readUnsignedIntToInt() & 0x000000FF;
+ sampleCount = data.readUnsignedIntToInt();
+ }
+
+ @Override
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ @Override
+ public int readNextSampleSize() {
+ if (fieldSize == 8) {
+ return data.readUnsignedByte();
+ } else if (fieldSize == 16) {
+ return data.readUnsignedShort();
+ } else {
+ // fieldSize == 4.
+ if ((sampleIndex++ % 2) == 0) {
+ // Read the next byte into our cached byte when we are reading the upper bits.
+ currentByte = data.readUnsignedByte();
+ // Read the upper bits from the byte and shift them to the lower 4 bits.
+ return (currentByte & 0xF0) >> 4;
+ } else {
+ // Mask out the upper 4 bits of the last byte we read.
+ return currentByte & 0x0F;
+ }
+ }
+ }
+
+ @Override
+ public boolean isFixedSampleSize() {
+ return false;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+/* package */ final class DefaultSampleValues {
+
+ public final int sampleDescriptionIndex;
+ public final int duration;
+ public final int size;
+ public final int flags;
+
+ public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) {
+ this.sampleDescriptionIndex = sampleDescriptionIndex;
+ this.duration = duration;
+ this.size = size;
+ this.flags = flags;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Rechunks fixed sample size media in which every sample is a key frame (e.g. uncompressed audio).
+ */
+/* package */ final class FixedSampleSizeRechunker {
+
+ /**
+ * The result of a rechunking operation.
+ */
+ public static final class Results {
+
+ public final long[] offsets;
+ public final int[] sizes;
+ public final int maximumSize;
+ public final long[] timestamps;
+ public final int[] flags;
+
+ private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) {
+ this.offsets = offsets;
+ this.sizes = sizes;
+ this.maximumSize = maximumSize;
+ this.timestamps = timestamps;
+ this.flags = flags;
+ }
+
+ }
+
+ /**
+ * Maximum number of bytes for each buffer in rechunked output.
+ */
+ private static final int MAX_SAMPLE_SIZE = 8 * 1024;
+
+ /**
+ * Rechunk the given fixed sample size input to produce a new sequence of samples.
+ *
+ * @param fixedSampleSize Size in bytes of each sample.
+ * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk.
+ * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks.
+ * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units.
+ */
+ public static Results rechunk(int fixedSampleSize, long[] chunkOffsets, int[] chunkSampleCounts,
+ long timestampDeltaInTimeUnits) {
+ int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize;
+
+ // Count the number of new, rechunked buffers.
+ int rechunkedSampleCount = 0;
+ for (int chunkSampleCount : chunkSampleCounts) {
+ rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount);
+ }
+
+ long[] offsets = new long[rechunkedSampleCount];
+ int[] sizes = new int[rechunkedSampleCount];
+ int maximumSize = 0;
+ long[] timestamps = new long[rechunkedSampleCount];
+ int[] flags = new int[rechunkedSampleCount];
+
+ int originalSampleIndex = 0;
+ int newSampleIndex = 0;
+ for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) {
+ int chunkSamplesRemaining = chunkSampleCounts[chunkIndex];
+ long sampleOffset = chunkOffsets[chunkIndex];
+
+ while (chunkSamplesRemaining > 0) {
+ int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining);
+
+ offsets[newSampleIndex] = sampleOffset;
+ sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount;
+ maximumSize = Math.max(maximumSize, sizes[newSampleIndex]);
+ timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex);
+ flags[newSampleIndex] = C.BUFFER_FLAG_KEY_FRAME;
+
+ sampleOffset += sizes[newSampleIndex];
+ originalSampleIndex += bufferSampleCount;
+
+ chunkSamplesRemaining -= bufferSampleCount;
+ newSampleIndex++;
+ }
+ }
+
+ return new Results(offsets, sizes, maximumSize, timestamps, flags);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -0,0 +1,1315 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.support.annotation.IntDef;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
+import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
+import com.google.android.exoplayer2.text.cea.CeaUtil;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Stack;
+import java.util.UUID;
+
+/**
+ * Facilitates the extraction of data from the fragmented mp4 container format.
+ */
+public final class FragmentedMp4Extractor implements Extractor {
+
+ /**
+ * Factory for {@link FragmentedMp4Extractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new FragmentedMp4Extractor()};
+ }
+
+ };
+
+ /**
+ * Flags controlling the behavior of the extractor.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
+ FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
+ FLAG_SIDELOADED})
+ public @interface Flags {}
+ /**
+ * Flag to work around an issue in some video streams where every frame is marked as a sync frame.
+ * The workaround overrides the sync frame flags in the stream, forcing them to false except for
+ * the first sample in each segment.
+ * <p>
+ * This flag does nothing if the stream is not a video stream.
+ */
+ public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
+ /**
+ * Flag to ignore any tfdt boxes in the stream.
+ */
+ public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2;
+ /**
+ * Flag to indicate that the extractor should output an event message metadata track. Any event
+ * messages in the stream will be delivered as samples to this track.
+ */
+ public static final int FLAG_ENABLE_EMSG_TRACK = 4;
+ /**
+ * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
+ * contained within SEI NAL units in the stream will be delivered as samples to this track.
+ */
+ public static final int FLAG_ENABLE_CEA608_TRACK = 8;
+ /**
+ * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
+ * container.
+ */
+ private static final int FLAG_SIDELOADED = 16;
+
+ private static final String TAG = "FragmentedMp4Extractor";
+ private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
+ private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
+ new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
+
+ // Parser states.
+ private static final int STATE_READING_ATOM_HEADER = 0;
+ private static final int STATE_READING_ATOM_PAYLOAD = 1;
+ private static final int STATE_READING_ENCRYPTION_DATA = 2;
+ private static final int STATE_READING_SAMPLE_START = 3;
+ private static final int STATE_READING_SAMPLE_CONTINUE = 4;
+
+ // Workarounds.
+ @Flags private final int flags;
+ private final Track sideloadedTrack;
+
+ // Track-linked data bundle, accessible as a whole through trackID.
+ private final SparseArray<TrackBundle> trackBundles;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalPrefix;
+ private final ParsableByteArray nalBuffer;
+ private final ParsableByteArray encryptionSignalByte;
+
+ // Adjusts sample timestamps.
+ private final TimestampAdjuster timestampAdjuster;
+
+ // Parser state.
+ private final ParsableByteArray atomHeader;
+ private final byte[] extendedTypeScratch;
+ private final Stack<ContainerAtom> containerAtoms;
+ private final LinkedList<MetadataSampleInfo> pendingMetadataSampleInfos;
+
+ private int parserState;
+ private int atomType;
+ private long atomSize;
+ private int atomHeaderBytesRead;
+ private ParsableByteArray atomData;
+ private long endOfMdatPosition;
+ private int pendingMetadataSampleBytes;
+
+ private long durationUs;
+ private long segmentIndexEarliestPresentationTimeUs;
+ private TrackBundle currentTrackBundle;
+ private int sampleSize;
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+ private boolean processSeiNalUnitPayload;
+
+ // Extractor output.
+ private ExtractorOutput extractorOutput;
+ private TrackOutput eventMessageTrackOutput;
+ private TrackOutput[] cea608TrackOutputs;
+
+ // Whether extractorOutput.seekMap has been called.
+ private boolean haveOutputSeekMap;
+
+ public FragmentedMp4Extractor() {
+ this(0);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ */
+ public FragmentedMp4Extractor(@Flags int flags) {
+ this(flags, null);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
+ */
+ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
+ this(flags, timestampAdjuster, null);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
+ * @param sideloadedTrack Sideloaded track information, in the case that the extractor
+ * will not receive a moov box in the input data.
+ */
+ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
+ Track sideloadedTrack) {
+ this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
+ this.timestampAdjuster = timestampAdjuster;
+ this.sideloadedTrack = sideloadedTrack;
+ atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalPrefix = new ParsableByteArray(5);
+ nalBuffer = new ParsableByteArray();
+ encryptionSignalByte = new ParsableByteArray(1);
+ extendedTypeScratch = new byte[16];
+ containerAtoms = new Stack<>();
+ pendingMetadataSampleInfos = new LinkedList<>();
+ trackBundles = new SparseArray<>();
+ durationUs = C.TIME_UNSET;
+ segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
+ enterReadingAtomHeaderState();
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return Sniffer.sniffFragmented(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ if (sideloadedTrack != null) {
+ TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type));
+ bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
+ trackBundles.put(0, bundle);
+ maybeInitExtraTracks();
+ extractorOutput.endTracks();
+ }
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ int trackCount = trackBundles.size();
+ for (int i = 0; i < trackCount; i++) {
+ trackBundles.valueAt(i).reset();
+ }
+ pendingMetadataSampleInfos.clear();
+ pendingMetadataSampleBytes = 0;
+ containerAtoms.clear();
+ enterReadingAtomHeaderState();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_ATOM_HEADER:
+ if (!readAtomHeader(input)) {
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_ATOM_PAYLOAD:
+ readAtomPayload(input);
+ break;
+ case STATE_READING_ENCRYPTION_DATA:
+ readEncryptionData(input);
+ break;
+ default:
+ if (readSample(input)) {
+ return RESULT_CONTINUE;
+ }
+ }
+ }
+ }
+
+ private void enterReadingAtomHeaderState() {
+ parserState = STATE_READING_ATOM_HEADER;
+ atomHeaderBytesRead = 0;
+ }
+
+ private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (atomHeaderBytesRead == 0) {
+ // Read the standard length atom header.
+ if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
+ return false;
+ }
+ atomHeaderBytesRead = Atom.HEADER_SIZE;
+ atomHeader.setPosition(0);
+ atomSize = atomHeader.readUnsignedInt();
+ atomType = atomHeader.readInt();
+ }
+
+ if (atomSize == Atom.LONG_SIZE_PREFIX) {
+ // Read the extended atom size.
+ int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
+ input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
+ atomHeaderBytesRead += headerBytesRemaining;
+ atomSize = atomHeader.readUnsignedLongToLong();
+ }
+
+ if (atomSize < atomHeaderBytesRead) {
+ throw new ParserException("Atom size less than header length (unsupported).");
+ }
+
+ long atomPosition = input.getPosition() - atomHeaderBytesRead;
+ if (atomType == Atom.TYPE_moof) {
+ // The data positions may be updated when parsing the tfhd/trun.
+ int trackCount = trackBundles.size();
+ for (int i = 0; i < trackCount; i++) {
+ TrackFragment fragment = trackBundles.valueAt(i).fragment;
+ fragment.atomPosition = atomPosition;
+ fragment.auxiliaryDataPosition = atomPosition;
+ fragment.dataPosition = atomPosition;
+ }
+ }
+
+ if (atomType == Atom.TYPE_mdat) {
+ currentTrackBundle = null;
+ endOfMdatPosition = atomPosition + atomSize;
+ if (!haveOutputSeekMap) {
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+ haveOutputSeekMap = true;
+ }
+ parserState = STATE_READING_ENCRYPTION_DATA;
+ return true;
+ }
+
+ if (shouldParseContainerAtom(atomType)) {
+ long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
+ containerAtoms.add(new ContainerAtom(atomType, endPosition));
+ if (atomSize == atomHeaderBytesRead) {
+ processAtomEnded(endPosition);
+ } else {
+ // Start reading the first child atom.
+ enterReadingAtomHeaderState();
+ }
+ } else if (shouldParseLeafAtom(atomType)) {
+ if (atomHeaderBytesRead != Atom.HEADER_SIZE) {
+ throw new ParserException("Leaf atom defines extended atom size (unsupported).");
+ }
+ if (atomSize > Integer.MAX_VALUE) {
+ throw new ParserException("Leaf atom with length > 2147483647 (unsupported).");
+ }
+ atomData = new ParsableByteArray((int) atomSize);
+ System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ } else {
+ if (atomSize > Integer.MAX_VALUE) {
+ throw new ParserException("Skipping atom with length > 2147483647 (unsupported).");
+ }
+ atomData = null;
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ }
+
+ return true;
+ }
+
+ private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {
+ int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;
+ if (atomData != null) {
+ input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize);
+ onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
+ } else {
+ input.skipFully(atomPayloadSize);
+ }
+ processAtomEnded(input.getPosition());
+ }
+
+ private void processAtomEnded(long atomEndPosition) throws ParserException {
+ while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
+ onContainerAtomRead(containerAtoms.pop());
+ }
+ enterReadingAtomHeaderState();
+ }
+
+ private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException {
+ if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(leaf);
+ } else if (leaf.type == Atom.TYPE_sidx) {
+ Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
+ segmentIndexEarliestPresentationTimeUs = result.first;
+ extractorOutput.seekMap(result.second);
+ haveOutputSeekMap = true;
+ } else if (leaf.type == Atom.TYPE_emsg) {
+ onEmsgLeafAtomRead(leaf.data);
+ }
+ }
+
+ private void onContainerAtomRead(ContainerAtom container) throws ParserException {
+ if (container.type == Atom.TYPE_moov) {
+ onMoovContainerAtomRead(container);
+ } else if (container.type == Atom.TYPE_moof) {
+ onMoofContainerAtomRead(container);
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(container);
+ }
+ }
+
+ private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException {
+ Assertions.checkState(sideloadedTrack == null, "Unexpected moov box.");
+
+ DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren);
+
+ // Read declaration of track fragments in the Moov box.
+ ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
+ SparseArray<DefaultSampleValues> defaultSampleValuesArray = new SparseArray<>();
+ long duration = C.TIME_UNSET;
+ int mvexChildrenSize = mvex.leafChildren.size();
+ for (int i = 0; i < mvexChildrenSize; i++) {
+ Atom.LeafAtom atom = mvex.leafChildren.get(i);
+ if (atom.type == Atom.TYPE_trex) {
+ Pair<Integer, DefaultSampleValues> trexData = parseTrex(atom.data);
+ defaultSampleValuesArray.put(trexData.first, trexData.second);
+ } else if (atom.type == Atom.TYPE_mehd) {
+ duration = parseMehd(atom.data);
+ }
+ }
+
+ // Construction of tracks.
+ SparseArray<Track> tracks = new SparseArray<>();
+ int moovContainerChildrenSize = moov.containerChildren.size();
+ for (int i = 0; i < moovContainerChildrenSize; i++) {
+ Atom.ContainerAtom atom = moov.containerChildren.get(i);
+ if (atom.type == Atom.TYPE_trak) {
+ Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration,
+ drmInitData, false);
+ if (track != null) {
+ tracks.put(track.id, track);
+ }
+ }
+ }
+
+ int trackCount = tracks.size();
+ if (trackBundles.size() == 0) {
+ // We need to create the track bundles.
+ for (int i = 0; i < trackCount; i++) {
+ Track track = tracks.valueAt(i);
+ TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
+ trackBundle.init(track, defaultSampleValuesArray.get(track.id));
+ trackBundles.put(track.id, trackBundle);
+ durationUs = Math.max(durationUs, track.durationUs);
+ }
+ maybeInitExtraTracks();
+ extractorOutput.endTracks();
+ } else {
+ Assertions.checkState(trackBundles.size() == trackCount);
+ for (int i = 0; i < trackCount; i++) {
+ Track track = tracks.valueAt(i);
+ trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
+ }
+ }
+ }
+
+ private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
+ parseMoof(moof, trackBundles, flags, extendedTypeScratch);
+ DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren);
+ if (drmInitData != null) {
+ int trackCount = trackBundles.size();
+ for (int i = 0; i < trackCount; i++) {
+ trackBundles.valueAt(i).updateDrmInitData(drmInitData);
+ }
+ }
+ }
+
+ private void maybeInitExtraTracks() {
+ if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
+ eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA);
+ eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
+ Format.OFFSET_SAMPLE_RELATIVE));
+ }
+ if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) {
+ TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1,
+ C.TRACK_TYPE_TEXT);
+ cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
+ null, Format.NO_VALUE, 0, null, null));
+ cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
+ }
+ }
+
+ /**
+ * Handles an emsg atom (defined in 23009-1).
+ */
+ private void onEmsgLeafAtomRead(ParsableByteArray atom) {
+ if (eventMessageTrackOutput == null) {
+ return;
+ }
+ // Parse the event's presentation time delta.
+ atom.setPosition(Atom.FULL_HEADER_SIZE);
+ atom.readNullTerminatedString(); // schemeIdUri
+ atom.readNullTerminatedString(); // value
+ long timescale = atom.readUnsignedInt();
+ long presentationTimeDeltaUs =
+ Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
+ // Output the sample data.
+ atom.setPosition(Atom.FULL_HEADER_SIZE);
+ int sampleSize = atom.bytesLeft();
+ eventMessageTrackOutput.sampleData(atom, sampleSize);
+ // Output the sample metadata.
+ if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
+ // We can output the sample metadata immediately.
+ eventMessageTrackOutput.sampleMetadata(
+ segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
+ C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
+ } else {
+ // We need the first sample timestamp in the segment before we can output the metadata.
+ pendingMetadataSampleInfos.addLast(
+ new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
+ pendingMetadataSampleBytes += sampleSize;
+ }
+ }
+
+ /**
+ * Parses a trex atom (defined in 14496-12).
+ */
+ private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) {
+ trex.setPosition(Atom.FULL_HEADER_SIZE);
+ int trackId = trex.readInt();
+ int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
+ int defaultSampleDuration = trex.readUnsignedIntToInt();
+ int defaultSampleSize = trex.readUnsignedIntToInt();
+ int defaultSampleFlags = trex.readInt();
+
+ return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex,
+ defaultSampleDuration, defaultSampleSize, defaultSampleFlags));
+ }
+
+ /**
+ * Parses an mehd atom (defined in 14496-12).
+ */
+ private static long parseMehd(ParsableByteArray mehd) {
+ mehd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = mehd.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong();
+ }
+
+ private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray,
+ @Flags int flags, byte[] extendedTypeScratch) throws ParserException {
+ int moofContainerChildrenSize = moof.containerChildren.size();
+ for (int i = 0; i < moofContainerChildrenSize; i++) {
+ Atom.ContainerAtom child = moof.containerChildren.get(i);
+ // TODO: Support multiple traf boxes per track in a single moof.
+ if (child.type == Atom.TYPE_traf) {
+ parseTraf(child, trackBundleArray, flags, extendedTypeScratch);
+ }
+ }
+ }
+
+ /**
+ * Parses a traf atom (defined in 14496-12).
+ */
+ private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
+ @Flags int flags, byte[] extendedTypeScratch) throws ParserException {
+ LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
+ TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags);
+ if (trackBundle == null) {
+ return;
+ }
+
+ TrackFragment fragment = trackBundle.fragment;
+ long decodeTime = fragment.nextFragmentDecodeTime;
+ trackBundle.reset();
+
+ LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
+ if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) {
+ decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
+ }
+
+ parseTruns(traf, trackBundle, decodeTime, flags);
+
+ LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
+ if (saiz != null) {
+ TrackEncryptionBox trackEncryptionBox = trackBundle.track
+ .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex];
+ parseSaiz(trackEncryptionBox, saiz.data, fragment);
+ }
+
+ LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio);
+ if (saio != null) {
+ parseSaio(saio.data, fragment);
+ }
+
+ LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
+ if (senc != null) {
+ parseSenc(senc.data, fragment);
+ }
+
+ LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp);
+ LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd);
+ if (sbgp != null && sgpd != null) {
+ parseSgpd(sbgp.data, sgpd.data, fragment);
+ }
+
+ int leafChildrenSize = traf.leafChildren.size();
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom atom = traf.leafChildren.get(i);
+ if (atom.type == Atom.TYPE_uuid) {
+ parseUuid(atom.data, fragment, extendedTypeScratch);
+ }
+ }
+ }
+
+ private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime,
+ @Flags int flags) {
+ int trunCount = 0;
+ int totalSampleCount = 0;
+ List<LeafAtom> leafChildren = traf.leafChildren;
+ int leafChildrenSize = leafChildren.size();
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom atom = leafChildren.get(i);
+ if (atom.type == Atom.TYPE_trun) {
+ ParsableByteArray trunData = atom.data;
+ trunData.setPosition(Atom.FULL_HEADER_SIZE);
+ int trunSampleCount = trunData.readUnsignedIntToInt();
+ if (trunSampleCount > 0) {
+ totalSampleCount += trunSampleCount;
+ trunCount++;
+ }
+ }
+ }
+ trackBundle.currentTrackRunIndex = 0;
+ trackBundle.currentSampleInTrackRun = 0;
+ trackBundle.currentSampleIndex = 0;
+ trackBundle.fragment.initTables(trunCount, totalSampleCount);
+
+ int trunIndex = 0;
+ int trunStartPosition = 0;
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom trun = leafChildren.get(i);
+ if (trun.type == Atom.TYPE_trun) {
+ trunStartPosition = parseTrun(trackBundle, trunIndex++, decodeTime, flags, trun.data,
+ trunStartPosition);
+ }
+ }
+ }
+
+ private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
+ TrackFragment out) throws ParserException {
+ int vectorSize = encryptionBox.initializationVectorSize;
+ saiz.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = saiz.readInt();
+ int flags = Atom.parseFullAtomFlags(fullAtom);
+ if ((flags & 0x01) == 1) {
+ saiz.skipBytes(8);
+ }
+ int defaultSampleInfoSize = saiz.readUnsignedByte();
+
+ int sampleCount = saiz.readUnsignedIntToInt();
+ if (sampleCount != out.sampleCount) {
+ throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount);
+ }
+
+ int totalSize = 0;
+ if (defaultSampleInfoSize == 0) {
+ boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
+ for (int i = 0; i < sampleCount; i++) {
+ int sampleInfoSize = saiz.readUnsignedByte();
+ totalSize += sampleInfoSize;
+ sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
+ }
+ } else {
+ boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
+ totalSize += defaultSampleInfoSize * sampleCount;
+ Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
+ }
+ out.initEncryptionData(totalSize);
+ }
+
+ /**
+ * Parses a saio atom (defined in 14496-12).
+ *
+ * @param saio The saio atom to decode.
+ * @param out The {@link TrackFragment} to populate with data from the saio atom.
+ */
+ private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException {
+ saio.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = saio.readInt();
+ int flags = Atom.parseFullAtomFlags(fullAtom);
+ if ((flags & 0x01) == 1) {
+ saio.skipBytes(8);
+ }
+
+ int entryCount = saio.readUnsignedIntToInt();
+ if (entryCount != 1) {
+ // We only support one trun element currently, so always expect one entry.
+ throw new ParserException("Unexpected saio entry count: " + entryCount);
+ }
+
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ out.auxiliaryDataPosition +=
+ version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong();
+ }
+
+ /**
+ * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and
+ * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer
+ * to any {@link TrackBundle}, {@code null} is returned and no changes are made.
+ *
+ * @param tfhd The tfhd atom to decode.
+ * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed.
+ * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd
+ * does not refer to any {@link TrackBundle}.
+ */
+ private static TrackBundle parseTfhd(ParsableByteArray tfhd,
+ SparseArray<TrackBundle> trackBundles, int flags) {
+ tfhd.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tfhd.readInt();
+ int atomFlags = Atom.parseFullAtomFlags(fullAtom);
+ int trackId = tfhd.readInt();
+ TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0);
+ if (trackBundle == null) {
+ return null;
+ }
+ if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) {
+ long baseDataPosition = tfhd.readUnsignedLongToLong();
+ trackBundle.fragment.dataPosition = baseDataPosition;
+ trackBundle.fragment.auxiliaryDataPosition = baseDataPosition;
+ }
+
+ DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
+ int defaultSampleDescriptionIndex =
+ ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
+ int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
+ int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size;
+ int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0)
+ ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags;
+ trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex,
+ defaultSampleDuration, defaultSampleSize, defaultSampleFlags);
+ return trackBundle;
+ }
+
+ /**
+ * Parses a tfdt atom (defined in 14496-12).
+ *
+ * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
+ * media, expressed in the media's timescale.
+ */
+ private static long parseTfdt(ParsableByteArray tfdt) {
+ tfdt.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = tfdt.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+ return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
+ }
+
+ /**
+ * Parses a trun atom (defined in 14496-12).
+ *
+ * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into
+ * which parsed data should be placed.
+ * @param index Index of the track run in the fragment.
+ * @param decodeTime The decode time of the first sample in the fragment run.
+ * @param flags Flags to allow any required workaround to be executed.
+ * @param trun The trun atom to decode.
+ * @return The starting position of samples for the next run.
+ */
+ private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime,
+ @Flags int flags, ParsableByteArray trun, int trackRunStart) {
+ trun.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = trun.readInt();
+ int atomFlags = Atom.parseFullAtomFlags(fullAtom);
+
+ Track track = trackBundle.track;
+ TrackFragment fragment = trackBundle.fragment;
+ DefaultSampleValues defaultSampleValues = fragment.header;
+
+ fragment.trunLength[index] = trun.readUnsignedIntToInt();
+ fragment.trunDataPosition[index] = fragment.dataPosition;
+ if ((atomFlags & 0x01 /* data_offset_present */) != 0) {
+ fragment.trunDataPosition[index] += trun.readInt();
+ }
+
+ boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0;
+ int firstSampleFlags = defaultSampleValues.flags;
+ if (firstSampleFlagsPresent) {
+ firstSampleFlags = trun.readUnsignedIntToInt();
+ }
+
+ boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0;
+ boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0;
+ boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0;
+ boolean sampleCompositionTimeOffsetsPresent =
+ (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0;
+
+ // Offset to the entire video timeline. In the presence of B-frames this is usually used to
+ // ensure that the first frame's presentation timestamp is zero.
+ long edtsOffset = 0;
+
+ // Currently we only support a single edit that moves the entire media timeline (indicated by
+ // duration == 0). Other uses of edit lists are uncommon and unsupported.
+ if (track.editListDurations != null && track.editListDurations.length == 1
+ && track.editListDurations[0] == 0) {
+ edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale);
+ }
+
+ int[] sampleSizeTable = fragment.sampleSizeTable;
+ int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable;
+ long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable;
+ boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable;
+
+ boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO
+ && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0;
+
+ int trackRunEnd = trackRunStart + fragment.trunLength[index];
+ long timescale = track.timescale;
+ long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime;
+ for (int i = trackRunStart; i < trackRunEnd; i++) {
+ // Use trun values if present, otherwise tfhd, otherwise trex.
+ int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
+ : defaultSampleValues.duration;
+ int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
+ int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
+ : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
+ if (sampleCompositionTimeOffsetsPresent) {
+ // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
+ // version 0 trun boxes, however a significant number of streams violate the spec and use
+ // signed integers instead. It's safe to always decode sample offsets as signed integers
+ // here, because unsigned integers will still be parsed correctly (unless their top bit is
+ // set, which is never true in practice because sample offsets are always small).
+ int sampleOffset = trun.readInt();
+ sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
+ } else {
+ sampleCompositionTimeOffsetTable[i] = 0;
+ }
+ sampleDecodingTimeTable[i] =
+ Util.scaleLargeTimestamp(cumulativeTime, 1000, timescale) - edtsOffset;
+ sampleSizeTable[i] = sampleSize;
+ sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
+ && (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
+ cumulativeTime += sampleDuration;
+ }
+ fragment.nextFragmentDecodeTime = cumulativeTime;
+ return trackRunEnd;
+ }
+
+ private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
+ byte[] extendedTypeScratch) throws ParserException {
+ uuid.setPosition(Atom.HEADER_SIZE);
+ uuid.readBytes(extendedTypeScratch, 0, 16);
+
+ // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
+ if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
+ return;
+ }
+
+ // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
+ // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
+ // Section 5.3.2.1."
+ parseSenc(uuid, 16, out);
+ }
+
+ private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException {
+ parseSenc(senc, 0, out);
+ }
+
+ private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out)
+ throws ParserException {
+ senc.setPosition(Atom.HEADER_SIZE + offset);
+ int fullAtom = senc.readInt();
+ int flags = Atom.parseFullAtomFlags(fullAtom);
+
+ if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
+ // TODO: Implement this.
+ throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported.");
+ }
+
+ boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
+ int sampleCount = senc.readUnsignedIntToInt();
+ if (sampleCount != out.sampleCount) {
+ throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount);
+ }
+
+ Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
+ out.initEncryptionData(senc.bytesLeft());
+ out.fillEncryptionData(senc);
+ }
+
+ private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, TrackFragment out)
+ throws ParserException {
+ sbgp.setPosition(Atom.HEADER_SIZE);
+ int sbgpFullAtom = sbgp.readInt();
+ if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) {
+ // Only seig grouping type is supported.
+ return;
+ }
+ if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) {
+ sbgp.skipBytes(4);
+ }
+ if (sbgp.readInt() != 1) {
+ throw new ParserException("Entry count in sbgp != 1 (unsupported).");
+ }
+
+ sgpd.setPosition(Atom.HEADER_SIZE);
+ int sgpdFullAtom = sgpd.readInt();
+ if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) {
+ // Only seig grouping type is supported.
+ return;
+ }
+ int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom);
+ if (sgpdVersion == 1) {
+ if (sgpd.readUnsignedInt() == 0) {
+ throw new ParserException("Variable length decription in sgpd found (unsupported)");
+ }
+ } else if (sgpdVersion >= 2) {
+ sgpd.skipBytes(4);
+ }
+ if (sgpd.readUnsignedInt() != 1) {
+ throw new ParserException("Entry count in sgpd != 1 (unsupported).");
+ }
+ // CencSampleEncryptionInformationGroupEntry
+ sgpd.skipBytes(2);
+ boolean isProtected = sgpd.readUnsignedByte() == 1;
+ if (!isProtected) {
+ return;
+ }
+ int initVectorSize = sgpd.readUnsignedByte();
+ byte[] keyId = new byte[16];
+ sgpd.readBytes(keyId, 0, keyId.length);
+ out.definesEncryptionData = true;
+ out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId);
+ }
+
+ /**
+ * Parses a sidx atom (defined in 14496-12).
+ *
+ * @param atom The atom data.
+ * @param inputPosition The input position of the first byte after the atom.
+ * @return A pair consisting of the earliest presentation time in microseconds, and the parsed
+ * {@link ChunkIndex}.
+ */
+ private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)
+ throws ParserException {
+ atom.setPosition(Atom.HEADER_SIZE);
+ int fullAtom = atom.readInt();
+ int version = Atom.parseFullAtomVersion(fullAtom);
+
+ atom.skipBytes(4);
+ long timescale = atom.readUnsignedInt();
+ long earliestPresentationTime;
+ long offset = inputPosition;
+ if (version == 0) {
+ earliestPresentationTime = atom.readUnsignedInt();
+ offset += atom.readUnsignedInt();
+ } else {
+ earliestPresentationTime = atom.readUnsignedLongToLong();
+ offset += atom.readUnsignedLongToLong();
+ }
+ long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
+ C.MICROS_PER_SECOND, timescale);
+
+ atom.skipBytes(2);
+
+ int referenceCount = atom.readUnsignedShort();
+ int[] sizes = new int[referenceCount];
+ long[] offsets = new long[referenceCount];
+ long[] durationsUs = new long[referenceCount];
+ long[] timesUs = new long[referenceCount];
+
+ long time = earliestPresentationTime;
+ long timeUs = earliestPresentationTimeUs;
+ for (int i = 0; i < referenceCount; i++) {
+ int firstInt = atom.readInt();
+
+ int type = 0x80000000 & firstInt;
+ if (type != 0) {
+ throw new ParserException("Unhandled indirect reference");
+ }
+ long referenceDuration = atom.readUnsignedInt();
+
+ sizes[i] = 0x7FFFFFFF & firstInt;
+ offsets[i] = offset;
+
+ // Calculate time and duration values such that any rounding errors are consistent. i.e. That
+ // timesUs[i] + durationsUs[i] == timesUs[i + 1].
+ timesUs[i] = timeUs;
+ time += referenceDuration;
+ timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
+ durationsUs[i] = timeUs - timesUs[i];
+
+ atom.skipBytes(4);
+ offset += sizes[i];
+ }
+
+ return Pair.create(earliestPresentationTimeUs,
+ new ChunkIndex(sizes, offsets, durationsUs, timesUs));
+ }
+
+ private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
+ TrackBundle nextTrackBundle = null;
+ long nextDataOffset = Long.MAX_VALUE;
+ int trackBundlesSize = trackBundles.size();
+ for (int i = 0; i < trackBundlesSize; i++) {
+ TrackFragment trackFragment = trackBundles.valueAt(i).fragment;
+ if (trackFragment.sampleEncryptionDataNeedsFill
+ && trackFragment.auxiliaryDataPosition < nextDataOffset) {
+ nextDataOffset = trackFragment.auxiliaryDataPosition;
+ nextTrackBundle = trackBundles.valueAt(i);
+ }
+ }
+ if (nextTrackBundle == null) {
+ parserState = STATE_READING_SAMPLE_START;
+ return;
+ }
+ int bytesToSkip = (int) (nextDataOffset - input.getPosition());
+ if (bytesToSkip < 0) {
+ throw new ParserException("Offset to encryption data was negative.");
+ }
+ input.skipFully(bytesToSkip);
+ nextTrackBundle.fragment.fillEncryptionData(input);
+ }
+
+ /**
+ * Attempts to extract the next sample in the current mdat atom.
+ * <p>
+ * If there are no more samples in the current mdat atom then the parser state is transitioned
+ * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned.
+ * <p>
+ * It is possible for a sample to be extracted in part in the case that an exception is thrown. In
+ * this case the method can be called again to extract the remainder of the sample.
+ *
+ * @param input The {@link ExtractorInput} from which to read data.
+ * @return Whether a sample was extracted.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
+ if (parserState == STATE_READING_SAMPLE_START) {
+ if (currentTrackBundle == null) {
+ TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles);
+ if (currentTrackBundle == null) {
+ // We've run out of samples in the current mdat. Discard any trailing data and prepare to
+ // read the header of the next atom.
+ int bytesToSkip = (int) (endOfMdatPosition - input.getPosition());
+ if (bytesToSkip < 0) {
+ throw new ParserException("Offset to end of mdat was negative.");
+ }
+ input.skipFully(bytesToSkip);
+ enterReadingAtomHeaderState();
+ return false;
+ }
+
+ long nextDataPosition = currentTrackBundle.fragment
+ .trunDataPosition[currentTrackBundle.currentTrackRunIndex];
+ // We skip bytes preceding the next sample to read.
+ int bytesToSkip = (int) (nextDataPosition - input.getPosition());
+ if (bytesToSkip < 0) {
+ // Assume the sample data must be contiguous in the mdat with no preceding data.
+ Log.w(TAG, "Ignoring negative offset to sample data.");
+ bytesToSkip = 0;
+ }
+ input.skipFully(bytesToSkip);
+ this.currentTrackBundle = currentTrackBundle;
+ }
+ sampleSize = currentTrackBundle.fragment
+ .sampleSizeTable[currentTrackBundle.currentSampleIndex];
+ if (currentTrackBundle.fragment.definesEncryptionData) {
+ sampleBytesWritten = appendSampleEncryptionData(currentTrackBundle);
+ sampleSize += sampleBytesWritten;
+ } else {
+ sampleBytesWritten = 0;
+ }
+ if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
+ sampleSize -= Atom.HEADER_SIZE;
+ input.skipFully(Atom.HEADER_SIZE);
+ }
+ parserState = STATE_READING_SAMPLE_CONTINUE;
+ sampleCurrentNalBytesRemaining = 0;
+ }
+
+ TrackFragment fragment = currentTrackBundle.fragment;
+ Track track = currentTrackBundle.track;
+ TrackOutput output = currentTrackBundle.output;
+ int sampleIndex = currentTrackBundle.currentSampleIndex;
+ if (track.nalUnitLengthFieldLength != 0) {
+ // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
+ // they're only 1 or 2 bytes long.
+ byte[] nalPrefixData = nalPrefix.data;
+ nalPrefixData[0] = 0;
+ nalPrefixData[1] = 0;
+ nalPrefixData[2] = 0;
+ int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1;
+ int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ while (sampleBytesWritten < sampleSize) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // Read the NAL length so that we know where we find the next one, and its type.
+ input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength);
+ nalPrefix.setPosition(0);
+ sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1;
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ output.sampleData(nalStartCode, 4);
+ // Write the NAL unit type byte.
+ output.sampleData(nalPrefix, 1);
+ processSeiNalUnitPayload = cea608TrackOutputs != null
+ && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
+ sampleBytesWritten += 5;
+ sampleSize += nalUnitLengthFieldLengthDiff;
+ } else {
+ int writtenBytes;
+ if (processSeiNalUnitPayload) {
+ // Read and write the payload of the SEI NAL unit.
+ nalBuffer.reset(sampleCurrentNalBytesRemaining);
+ input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining);
+ output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining);
+ writtenBytes = sampleCurrentNalBytesRemaining;
+ // Unescape and process the SEI NAL unit.
+ int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit());
+ // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte.
+ nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
+ nalBuffer.setLimit(unescapedLength);
+ CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer,
+ cea608TrackOutputs);
+ } else {
+ // Write the payload of the NAL unit.
+ writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
+ }
+ sampleBytesWritten += writtenBytes;
+ sampleCurrentNalBytesRemaining -= writtenBytes;
+ }
+ }
+ } else {
+ while (sampleBytesWritten < sampleSize) {
+ int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);
+ sampleBytesWritten += writtenBytes;
+ }
+ }
+
+ long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
+ @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
+ | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
+ int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
+ byte[] encryptionKey = null;
+ if (fragment.definesEncryptionData) {
+ encryptionKey = fragment.trackEncryptionBox != null
+ ? fragment.trackEncryptionBox.keyId
+ : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId;
+ }
+ if (timestampAdjuster != null) {
+ sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
+ }
+ output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
+
+ while (!pendingMetadataSampleInfos.isEmpty()) {
+ MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
+ pendingMetadataSampleBytes -= sampleInfo.size;
+ eventMessageTrackOutput.sampleMetadata(
+ sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
+ C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
+ }
+
+ currentTrackBundle.currentSampleIndex++;
+ currentTrackBundle.currentSampleInTrackRun++;
+ if (currentTrackBundle.currentSampleInTrackRun
+ == fragment.trunLength[currentTrackBundle.currentTrackRunIndex]) {
+ currentTrackBundle.currentTrackRunIndex++;
+ currentTrackBundle.currentSampleInTrackRun = 0;
+ currentTrackBundle = null;
+ }
+ parserState = STATE_READING_SAMPLE_START;
+ return true;
+ }
+
+ /**
+ * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those
+ * yet to be consumed, or null if all have been consumed.
+ */
+ private static TrackBundle getNextFragmentRun(SparseArray<TrackBundle> trackBundles) {
+ TrackBundle nextTrackBundle = null;
+ long nextTrackRunOffset = Long.MAX_VALUE;
+
+ int trackBundlesSize = trackBundles.size();
+ for (int i = 0; i < trackBundlesSize; i++) {
+ TrackBundle trackBundle = trackBundles.valueAt(i);
+ if (trackBundle.currentTrackRunIndex == trackBundle.fragment.trunCount) {
+ // This track fragment contains no more runs in the next mdat box.
+ } else {
+ long trunOffset = trackBundle.fragment.trunDataPosition[trackBundle.currentTrackRunIndex];
+ if (trunOffset < nextTrackRunOffset) {
+ nextTrackBundle = trackBundle;
+ nextTrackRunOffset = trunOffset;
+ }
+ }
+ }
+ return nextTrackBundle;
+ }
+
+ /**
+ * Appends the corresponding encryption data to the {@link TrackOutput} contained in the given
+ * {@link TrackBundle}.
+ *
+ * @param trackBundle The {@link TrackBundle} that contains the {@link Track} for which the
+ * Sample encryption data must be output.
+ * @return The number of written bytes.
+ */
+ private int appendSampleEncryptionData(TrackBundle trackBundle) {
+ TrackFragment trackFragment = trackBundle.fragment;
+ ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData;
+ int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex;
+ TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null
+ ? trackFragment.trackEncryptionBox
+ : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
+ int vectorSize = encryptionBox.initializationVectorSize;
+ boolean subsampleEncryption = trackFragment
+ .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex];
+
+ // Write the signal byte, containing the vector size and the subsample encryption flag.
+ encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));
+ encryptionSignalByte.setPosition(0);
+ TrackOutput output = trackBundle.output;
+ output.sampleData(encryptionSignalByte, 1);
+ // Write the vector.
+ output.sampleData(sampleEncryptionData, vectorSize);
+ // If we don't have subsample encryption data, we're done.
+ if (!subsampleEncryption) {
+ return 1 + vectorSize;
+ }
+ // Write the subsample encryption data.
+ int subsampleCount = sampleEncryptionData.readUnsignedShort();
+ sampleEncryptionData.skipBytes(-2);
+ int subsampleDataLength = 2 + 6 * subsampleCount;
+ output.sampleData(sampleEncryptionData, subsampleDataLength);
+ return 1 + vectorSize + subsampleDataLength;
+ }
+
+
+ /** Returns DrmInitData from leaf atoms. */
+ private static DrmInitData getDrmInitDataFromAtoms(List<Atom.LeafAtom> leafChildren) {
+ ArrayList<SchemeData> schemeDatas = null;
+ int leafChildrenSize = leafChildren.size();
+ for (int i = 0; i < leafChildrenSize; i++) {
+ LeafAtom child = leafChildren.get(i);
+ if (child.type == Atom.TYPE_pssh) {
+ if (schemeDatas == null) {
+ schemeDatas = new ArrayList<>();
+ }
+ byte[] psshData = child.data.data;
+ UUID uuid = PsshAtomUtil.parseUuid(psshData);
+ if (uuid == null) {
+ Log.w(TAG, "Skipped pssh atom (failed to extract uuid)");
+ } else {
+ schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData));
+ }
+ }
+ }
+ return schemeDatas == null ? null : new DrmInitData(schemeDatas);
+ }
+
+ /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
+ private static boolean shouldParseLeafAtom(int atom) {
+ return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd
+ || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt
+ || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex
+ || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz
+ || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid
+ || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst
+ || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg;
+ }
+
+ /** Returns whether the extractor should decode a container atom with type {@code atom}. */
+ private static boolean shouldParseContainerAtom(int atom) {
+ return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
+ || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_moof
+ || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts;
+ }
+
+ /**
+ * Holds data corresponding to a metadata sample.
+ */
+ private static final class MetadataSampleInfo {
+
+ public final long presentationTimeDeltaUs;
+ public final int size;
+
+ public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
+ this.presentationTimeDeltaUs = presentationTimeDeltaUs;
+ this.size = size;
+ }
+
+ }
+
+ /**
+ * Holds data corresponding to a single track.
+ */
+ private static final class TrackBundle {
+
+ public final TrackFragment fragment;
+ public final TrackOutput output;
+
+ public Track track;
+ public DefaultSampleValues defaultSampleValues;
+ public int currentSampleIndex;
+ public int currentSampleInTrackRun;
+ public int currentTrackRunIndex;
+
+ public TrackBundle(TrackOutput output) {
+ fragment = new TrackFragment();
+ this.output = output;
+ }
+
+ public void init(Track track, DefaultSampleValues defaultSampleValues) {
+ this.track = Assertions.checkNotNull(track);
+ this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues);
+ output.format(track.format);
+ reset();
+ }
+
+ public void reset() {
+ fragment.reset();
+ currentSampleIndex = 0;
+ currentTrackRunIndex = 0;
+ currentSampleInTrackRun = 0;
+ }
+
+ public void updateDrmInitData(DrmInitData drmInitData) {
+ output.format(track.format.copyWithDrmInitData(drmInitData));
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -0,0 +1,317 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Log;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.ApicFrame;
+import com.google.android.exoplayer2.metadata.id3.CommentFrame;
+import com.google.android.exoplayer2.metadata.id3.Id3Frame;
+import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Parses metadata items stored in ilst atoms.
+ */
+/* package */ final class MetadataUtil {
+
+ private static final String TAG = "MetadataUtil";
+
+ // Codes that start with the copyright character (omitted) and have equivalent ID3 frames.
+ private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam");
+ private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk");
+ private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt");
+ private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day");
+ private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART");
+ private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too");
+ private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb");
+ private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com");
+ private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt");
+ private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr");
+ private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen");
+
+ // Codes that have equivalent ID3 frames.
+ private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr");
+ private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre");
+ private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp");
+ private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk");
+ private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn");
+ private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo");
+ private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil");
+ private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART");
+ private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm");
+ private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal");
+ private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar");
+ private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa");
+ private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco");
+
+ // Types that do not have equivalent ID3 frames.
+ private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng");
+ private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap");
+ private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn");
+ private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh");
+
+ // Type for items that are intended for internal use by the player.
+ private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----");
+
+ // Standard genres.
+ private static final String[] STANDARD_GENRES = new String[] {
+ // These are the official ID3v1 genres.
+ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
+ "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno",
+ "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno",
+ "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
+ "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul",
+ "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+ "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream",
+ "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle",
+ "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
+ "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll",
+ "Hard Rock",
+ // These were made up by the authors of Winamp and later added to the ID3 spec.
+ "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock",
+ "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
+ "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+ "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad",
+ "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella",
+ "Euro-House", "Dance Hall",
+ // These were med up by the authors of Winamp but have not been added to the ID3 spec.
+ "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk",
+ "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover",
+ "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime",
+ "Jpop", "Synthpop"
+ };
+
+ private static final String LANGUAGE_UNDEFINED = "und";
+
+ private MetadataUtil() {}
+
+ /**
+ * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
+ * from the current position of the {@link ParsableByteArray}, and the position is advanced by
+ * the size of the element. The position is advanced even if the element's type is unrecognized.
+ *
+ * @param ilst Holds the data to be parsed.
+ * @return The parsed element, or null if the element's type was not recognized.
+ */
+ public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
+ int position = ilst.getPosition();
+ int endPosition = position + ilst.readInt();
+ int type = ilst.readInt();
+ int typeTopByte = (type >> 24) & 0xFF;
+ try {
+ if (typeTopByte == '\u00A9' /* Copyright char */
+ || typeTopByte == '\uFFFD' /* Replacement char */) {
+ int shortType = type & 0x00FFFFFF;
+ if (shortType == SHORT_TYPE_COMMENT) {
+ return parseCommentAttribute(type, ilst);
+ } else if (shortType == SHORT_TYPE_NAME_1 || shortType == SHORT_TYPE_NAME_2) {
+ return parseTextAttribute(type, "TIT2", ilst);
+ } else if (shortType == SHORT_TYPE_COMPOSER_1 || shortType == SHORT_TYPE_COMPOSER_2) {
+ return parseTextAttribute(type, "TCOM", ilst);
+ } else if (shortType == SHORT_TYPE_YEAR) {
+ return parseTextAttribute(type, "TDRC", ilst);
+ } else if (shortType == SHORT_TYPE_ARTIST) {
+ return parseTextAttribute(type, "TPE1", ilst);
+ } else if (shortType == SHORT_TYPE_ENCODER) {
+ return parseTextAttribute(type, "TSSE", ilst);
+ } else if (shortType == SHORT_TYPE_ALBUM) {
+ return parseTextAttribute(type, "TALB", ilst);
+ } else if (shortType == SHORT_TYPE_LYRICS) {
+ return parseTextAttribute(type, "USLT", ilst);
+ } else if (shortType == SHORT_TYPE_GENRE) {
+ return parseTextAttribute(type, "TCON", ilst);
+ } else if (shortType == TYPE_GROUPING) {
+ return parseTextAttribute(type, "TIT1", ilst);
+ }
+ } else if (type == TYPE_GENRE) {
+ return parseStandardGenreAttribute(ilst);
+ } else if (type == TYPE_DISK_NUMBER) {
+ return parseIndexAndCountAttribute(type, "TPOS", ilst);
+ } else if (type == TYPE_TRACK_NUMBER) {
+ return parseIndexAndCountAttribute(type, "TRCK", ilst);
+ } else if (type == TYPE_TEMPO) {
+ return parseUint8Attribute(type, "TBPM", ilst, true, false);
+ } else if (type == TYPE_COMPILATION) {
+ return parseUint8Attribute(type, "TCMP", ilst, true, true);
+ } else if (type == TYPE_COVER_ART) {
+ return parseCoverArt(ilst);
+ } else if (type == TYPE_ALBUM_ARTIST) {
+ return parseTextAttribute(type, "TPE2", ilst);
+ } else if (type == TYPE_SORT_TRACK_NAME) {
+ return parseTextAttribute(type, "TSOT", ilst);
+ } else if (type == TYPE_SORT_ALBUM) {
+ return parseTextAttribute(type, "TSO2", ilst);
+ } else if (type == TYPE_SORT_ARTIST) {
+ return parseTextAttribute(type, "TSOA", ilst);
+ } else if (type == TYPE_SORT_ALBUM_ARTIST) {
+ return parseTextAttribute(type, "TSOP", ilst);
+ } else if (type == TYPE_SORT_COMPOSER) {
+ return parseTextAttribute(type, "TSOC", ilst);
+ } else if (type == TYPE_RATING) {
+ return parseUint8Attribute(type, "ITUNESADVISORY", ilst, false, false);
+ } else if (type == TYPE_GAPLESS_ALBUM) {
+ return parseUint8Attribute(type, "ITUNESGAPLESS", ilst, false, true);
+ } else if (type == TYPE_TV_SORT_SHOW) {
+ return parseTextAttribute(type, "TVSHOWSORT", ilst);
+ } else if (type == TYPE_TV_SHOW) {
+ return parseTextAttribute(type, "TVSHOW", ilst);
+ } else if (type == TYPE_INTERNAL) {
+ return parseInternalAttribute(ilst, endPosition);
+ }
+ Log.d(TAG, "Skipped unknown metadata entry: " + Atom.getAtomTypeString(type));
+ return null;
+ } finally {
+ ilst.setPosition(endPosition);
+ }
+ }
+
+ private static TextInformationFrame parseTextAttribute(int type, String id,
+ ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(atomSize - 16);
+ return new TextInformationFrame(id, null, value);
+ }
+ Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(atomSize - 16);
+ return new CommentFrame(LANGUAGE_UNDEFINED, value, value);
+ }
+ Log.w(TAG, "Failed to parse comment attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data,
+ boolean isTextInformationFrame, boolean isBoolean) {
+ int value = parseUint8AttributeValue(data);
+ if (isBoolean) {
+ value = Math.min(1, value);
+ }
+ if (value >= 0) {
+ return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value))
+ : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
+ }
+ Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName,
+ ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data && atomSize >= 22) {
+ data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
+ int index = data.readUnsignedShort();
+ if (index > 0) {
+ String value = "" + index;
+ int count = data.readUnsignedShort();
+ if (count > 0) {
+ value += "/" + count;
+ }
+ return new TextInformationFrame(attributeName, null, value);
+ }
+ }
+ Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
+ return null;
+ }
+
+ private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
+ int genreCode = parseUint8AttributeValue(data);
+ String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
+ ? STANDARD_GENRES[genreCode - 1] : null;
+ if (genreString != null) {
+ return new TextInformationFrame("TCON", null, genreString);
+ }
+ Log.w(TAG, "Failed to parse standard genre code");
+ return null;
+ }
+
+ private static ApicFrame parseCoverArt(ParsableByteArray data) {
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ int fullVersionInt = data.readInt();
+ int flags = Atom.parseFullAtomFlags(fullVersionInt);
+ String mimeType = flags == 13 ? "image/jpeg" : flags == 14 ? "image/png" : null;
+ if (mimeType == null) {
+ Log.w(TAG, "Unrecognized cover art flags: " + flags);
+ return null;
+ }
+ data.skipBytes(4); // empty (4)
+ byte[] pictureData = new byte[atomSize - 16];
+ data.readBytes(pictureData, 0, pictureData.length);
+ return new ApicFrame(mimeType, null, 3 /* Cover (front) */, pictureData);
+ }
+ Log.w(TAG, "Failed to parse cover art attribute");
+ return null;
+ }
+
+ private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
+ String domain = null;
+ String name = null;
+ int dataAtomPosition = -1;
+ int dataAtomSize = -1;
+ while (data.getPosition() < endPosition) {
+ int atomPosition = data.getPosition();
+ int atomSize = data.readInt();
+ int atomType = data.readInt();
+ data.skipBytes(4); // version (1), flags (3)
+ if (atomType == Atom.TYPE_mean) {
+ domain = data.readNullTerminatedString(atomSize - 12);
+ } else if (atomType == Atom.TYPE_name) {
+ name = data.readNullTerminatedString(atomSize - 12);
+ } else {
+ if (atomType == Atom.TYPE_data) {
+ dataAtomPosition = atomPosition;
+ dataAtomSize = atomSize;
+ }
+ data.skipBytes(atomSize - 12);
+ }
+ }
+ if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) {
+ // We're only interested in iTunSMPB.
+ return null;
+ }
+ data.setPosition(dataAtomPosition);
+ data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
+ String value = data.readNullTerminatedString(dataAtomSize - 16);
+ return new CommentFrame(LANGUAGE_UNDEFINED, name, value);
+ }
+
+ private static int parseUint8AttributeValue(ParsableByteArray data) {
+ data.skipBytes(4); // atomSize
+ int atomType = data.readInt();
+ if (atomType == Atom.TYPE_data) {
+ data.skipBytes(8); // version (1), flags (3), empty (4)
+ return data.readUnsignedByte();
+ }
+ Log.w(TAG, "Failed to parse uint8 attribute value");
+ return -1;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -0,0 +1,536 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * Extracts data from an unfragmented MP4 file.
+ */
+public final class Mp4Extractor implements Extractor, SeekMap {
+
+ /**
+ * Factory for {@link Mp4Extractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new Mp4Extractor()};
+ }
+
+ };
+
+ /**
+ * Parser states.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE})
+ private @interface State {}
+ private static final int STATE_READING_ATOM_HEADER = 0;
+ private static final int STATE_READING_ATOM_PAYLOAD = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ // Brand stored in the ftyp atom for QuickTime media.
+ private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt ");
+
+ /**
+ * When seeking within the source, if the offset is greater than or equal to this value (or the
+ * offset is negative), the source will be reloaded.
+ */
+ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
+
+ // Temporary arrays.
+ private final ParsableByteArray nalStartCode;
+ private final ParsableByteArray nalLength;
+
+ private final ParsableByteArray atomHeader;
+ private final Stack<ContainerAtom> containerAtoms;
+
+ @State private int parserState;
+ private int atomType;
+ private long atomSize;
+ private int atomHeaderBytesRead;
+ private ParsableByteArray atomData;
+
+ private int sampleBytesWritten;
+ private int sampleCurrentNalBytesRemaining;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+ private Mp4Track[] tracks;
+ private long durationUs;
+ private boolean isQuickTime;
+
+ public Mp4Extractor() {
+ atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
+ containerAtoms = new Stack<>();
+ nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+ nalLength = new ParsableByteArray(4);
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return Sniffer.sniffUnfragmented(input);
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ containerAtoms.clear();
+ atomHeaderBytesRead = 0;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ if (position == 0) {
+ enterReadingAtomHeaderState();
+ } else if (tracks != null) {
+ updateSampleIndices(timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_ATOM_HEADER:
+ if (!readAtomHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_ATOM_PAYLOAD:
+ if (readAtomPayload(input, seekPosition)) {
+ return RESULT_SEEK;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ return readSample(input, seekPosition);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ long earliestSamplePosition = Long.MAX_VALUE;
+ for (Mp4Track track : tracks) {
+ TrackSampleTable sampleTable = track.sampleTable;
+ int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ // Handle the case where the requested time is before the first synchronization sample.
+ sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ }
+ long offset = sampleTable.offsets[sampleIndex];
+ if (offset < earliestSamplePosition) {
+ earliestSamplePosition = offset;
+ }
+ }
+ return earliestSamplePosition;
+ }
+
+ // Private methods.
+
+ private void enterReadingAtomHeaderState() {
+ parserState = STATE_READING_ATOM_HEADER;
+ atomHeaderBytesRead = 0;
+ }
+
+ private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (atomHeaderBytesRead == 0) {
+ // Read the standard length atom header.
+ if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
+ return false;
+ }
+ atomHeaderBytesRead = Atom.HEADER_SIZE;
+ atomHeader.setPosition(0);
+ atomSize = atomHeader.readUnsignedInt();
+ atomType = atomHeader.readInt();
+ }
+
+ if (atomSize == Atom.LONG_SIZE_PREFIX) {
+ // Read the extended atom size.
+ int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
+ input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
+ atomHeaderBytesRead += headerBytesRemaining;
+ atomSize = atomHeader.readUnsignedLongToLong();
+ }
+
+ if (shouldParseContainerAtom(atomType)) {
+ long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
+ containerAtoms.add(new ContainerAtom(atomType, endPosition));
+ if (atomSize == atomHeaderBytesRead) {
+ processAtomEnded(endPosition);
+ } else {
+ // Start reading the first child atom.
+ enterReadingAtomHeaderState();
+ }
+ } else if (shouldParseLeafAtom(atomType)) {
+ // We don't support parsing of leaf atoms that define extended atom sizes, or that have
+ // lengths greater than Integer.MAX_VALUE.
+ Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE);
+ Assertions.checkState(atomSize <= Integer.MAX_VALUE);
+ atomData = new ParsableByteArray((int) atomSize);
+ System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ } else {
+ atomData = null;
+ parserState = STATE_READING_ATOM_PAYLOAD;
+ }
+
+ return true;
+ }
+
+ /**
+ * Processes the atom payload. If {@link #atomData} is null and the size is at or above the
+ * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should
+ * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.
+ */
+ private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)
+ throws IOException, InterruptedException {
+ long atomPayloadSize = atomSize - atomHeaderBytesRead;
+ long atomEndPosition = input.getPosition() + atomPayloadSize;
+ boolean seekRequired = false;
+ if (atomData != null) {
+ input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize);
+ if (atomType == Atom.TYPE_ftyp) {
+ isQuickTime = processFtypAtom(atomData);
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
+ }
+ } else {
+ // We don't need the data. Skip or seek, depending on how large the atom is.
+ if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) {
+ input.skipFully((int) atomPayloadSize);
+ } else {
+ positionHolder.position = input.getPosition() + atomPayloadSize;
+ seekRequired = true;
+ }
+ }
+ processAtomEnded(atomEndPosition);
+ return seekRequired && parserState != STATE_READING_SAMPLE;
+ }
+
+ private void processAtomEnded(long atomEndPosition) throws ParserException {
+ while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
+ Atom.ContainerAtom containerAtom = containerAtoms.pop();
+ if (containerAtom.type == Atom.TYPE_moov) {
+ // We've reached the end of the moov atom. Process it and prepare to read samples.
+ processMoovAtom(containerAtom);
+ containerAtoms.clear();
+ parserState = STATE_READING_SAMPLE;
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(containerAtom);
+ }
+ }
+ if (parserState != STATE_READING_SAMPLE) {
+ enterReadingAtomHeaderState();
+ }
+ }
+
+ /**
+ * Process an ftyp atom to determine whether the media is QuickTime.
+ *
+ * @param atomData The ftyp atom data.
+ * @return Whether the media is QuickTime.
+ */
+ private static boolean processFtypAtom(ParsableByteArray atomData) {
+ atomData.setPosition(Atom.HEADER_SIZE);
+ int majorBrand = atomData.readInt();
+ if (majorBrand == BRAND_QUICKTIME) {
+ return true;
+ }
+ atomData.skipBytes(4); // minor_version
+ while (atomData.bytesLeft() > 0) {
+ if (atomData.readInt() == BRAND_QUICKTIME) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Updates the stored track metadata to reflect the contents of the specified moov atom.
+ */
+ private void processMoovAtom(ContainerAtom moov) throws ParserException {
+ long durationUs = C.TIME_UNSET;
+ List<Mp4Track> tracks = new ArrayList<>();
+ long earliestSampleOffset = Long.MAX_VALUE;
+
+ Metadata metadata = null;
+ GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
+ Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
+ if (udta != null) {
+ metadata = AtomParsers.parseUdta(udta, isQuickTime);
+ if (metadata != null) {
+ gaplessInfoHolder.setFromMetadata(metadata);
+ }
+ }
+
+ for (int i = 0; i < moov.containerChildren.size(); i++) {
+ Atom.ContainerAtom atom = moov.containerChildren.get(i);
+ if (atom.type != Atom.TYPE_trak) {
+ continue;
+ }
+
+ Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd),
+ C.TIME_UNSET, null, isQuickTime);
+ if (track == null) {
+ continue;
+ }
+
+ Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
+ .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
+ TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
+ if (trackSampleTable.sampleCount == 0) {
+ continue;
+ }
+
+ Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,
+ extractorOutput.track(i, track.type));
+ // Each sample has up to three bytes of overhead for the start code that replaces its length.
+ // Allow ten source samples per output sample, like the platform extractor.
+ int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
+ Format format = track.format.copyWithMaxInputSize(maxInputSize);
+ if (track.type == C.TRACK_TYPE_AUDIO) {
+ if (gaplessInfoHolder.hasGaplessInfo()) {
+ format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
+ gaplessInfoHolder.encoderPadding);
+ }
+ if (metadata != null) {
+ format = format.copyWithMetadata(metadata);
+ }
+ }
+ mp4Track.trackOutput.format(format);
+
+ durationUs = Math.max(durationUs, track.durationUs);
+ tracks.add(mp4Track);
+
+ long firstSampleOffset = trackSampleTable.offsets[0];
+ if (firstSampleOffset < earliestSampleOffset) {
+ earliestSampleOffset = firstSampleOffset;
+ }
+ }
+ this.durationUs = durationUs;
+ this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
+ extractorOutput.endTracks();
+ extractorOutput.seekMap(this);
+ }
+
+ /**
+ * Attempts to extract the next sample in the current mdat atom for the specified track.
+ * <p>
+ * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in
+ * {@code positionHolder}.
+ * <p>
+ * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns
+ * {@link #RESULT_CONTINUE}.
+ *
+ * @param input The {@link ExtractorInput} from which to read data.
+ * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
+ * position of the required data.
+ * @return One of the {@code RESULT_*} flags in {@link Extractor}.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ private int readSample(ExtractorInput input, PositionHolder positionHolder)
+ throws IOException, InterruptedException {
+ int trackIndex = getTrackIndexOfEarliestCurrentSample();
+ if (trackIndex == C.INDEX_UNSET) {
+ return RESULT_END_OF_INPUT;
+ }
+ Mp4Track track = tracks[trackIndex];
+ TrackOutput trackOutput = track.trackOutput;
+ int sampleIndex = track.sampleIndex;
+ long position = track.sampleTable.offsets[sampleIndex];
+ int sampleSize = track.sampleTable.sizes[sampleIndex];
+ if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
+ // The sample information is contained in a cdat atom. The header must be discarded for
+ // committing.
+ position += Atom.HEADER_SIZE;
+ sampleSize -= Atom.HEADER_SIZE;
+ }
+ long skipAmount = position - input.getPosition() + sampleBytesWritten;
+ if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
+ positionHolder.position = position;
+ return RESULT_SEEK;
+ }
+ input.skipFully((int) skipAmount);
+ if (track.track.nalUnitLengthFieldLength != 0) {
+ // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
+ // they're only 1 or 2 bytes long.
+ byte[] nalLengthData = nalLength.data;
+ nalLengthData[0] = 0;
+ nalLengthData[1] = 0;
+ nalLengthData[2] = 0;
+ int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength;
+ int nalUnitLengthFieldLengthDiff = 4 - track.track.nalUnitLengthFieldLength;
+ // NAL units are length delimited, but the decoder requires start code delimited units.
+ // Loop until we've written the sample to the track output, replacing length delimiters with
+ // start codes as we encounter them.
+ while (sampleBytesWritten < sampleSize) {
+ if (sampleCurrentNalBytesRemaining == 0) {
+ // Read the NAL length so that we know where we find the next one.
+ input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+ nalLength.setPosition(0);
+ sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
+ // Write a start code for the current NAL unit.
+ nalStartCode.setPosition(0);
+ trackOutput.sampleData(nalStartCode, 4);
+ sampleBytesWritten += 4;
+ sampleSize += nalUnitLengthFieldLengthDiff;
+ } else {
+ // Write the payload of the NAL unit.
+ int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);
+ sampleBytesWritten += writtenBytes;
+ sampleCurrentNalBytesRemaining -= writtenBytes;
+ }
+ }
+ } else {
+ while (sampleBytesWritten < sampleSize) {
+ int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
+ sampleBytesWritten += writtenBytes;
+ sampleCurrentNalBytesRemaining -= writtenBytes;
+ }
+ }
+ trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
+ track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
+ track.sampleIndex++;
+ sampleBytesWritten = 0;
+ sampleCurrentNalBytesRemaining = 0;
+ return RESULT_CONTINUE;
+ }
+
+ /**
+ * Returns the index of the track that contains the earliest current sample, or
+ * {@link C#INDEX_UNSET} if no samples remain.
+ */
+ private int getTrackIndexOfEarliestCurrentSample() {
+ int earliestSampleTrackIndex = C.INDEX_UNSET;
+ long earliestSampleOffset = Long.MAX_VALUE;
+ for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
+ Mp4Track track = tracks[trackIndex];
+ int sampleIndex = track.sampleIndex;
+ if (sampleIndex == track.sampleTable.sampleCount) {
+ continue;
+ }
+
+ long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
+ if (trackSampleOffset < earliestSampleOffset) {
+ earliestSampleOffset = trackSampleOffset;
+ earliestSampleTrackIndex = trackIndex;
+ }
+ }
+
+ return earliestSampleTrackIndex;
+ }
+
+ /**
+ * Updates every track's sample index to point its latest sync sample before/at {@code timeUs}.
+ */
+ private void updateSampleIndices(long timeUs) {
+ for (Mp4Track track : tracks) {
+ TrackSampleTable sampleTable = track.sampleTable;
+ int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ // Handle the case where the requested time is before the first synchronization sample.
+ sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ }
+ track.sampleIndex = sampleIndex;
+ }
+ }
+
+ /**
+ * Returns whether the extractor should decode a leaf atom with type {@code atom}.
+ */
+ private static boolean shouldParseLeafAtom(int atom) {
+ return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
+ || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
+ || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
+ || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco
+ || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp
+ || atom == Atom.TYPE_udta;
+ }
+
+ /**
+ * Returns whether the extractor should decode a container atom with type {@code atom}.
+ */
+ private static boolean shouldParseContainerAtom(int atom) {
+ return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
+ || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts;
+ }
+
+ private static final class Mp4Track {
+
+ public final Track track;
+ public final TrackSampleTable sampleTable;
+ public final TrackOutput trackOutput;
+
+ public int sampleIndex;
+
+ public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {
+ this.track = track;
+ this.sampleTable = sampleTable;
+ this.trackOutput = trackOutput;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -0,0 +1,138 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+
+/**
+ * Utility methods for handling PSSH atoms.
+ */
+public final class PsshAtomUtil {
+
+ private static final String TAG = "PsshAtomUtil";
+
+ private PsshAtomUtil() {}
+
+ /**
+ * Builds a PSSH atom for a given {@link UUID} containing the given scheme specific data.
+ *
+ * @param uuid The UUID of the scheme.
+ * @param data The scheme specific data.
+ * @return The PSSH atom.
+ */
+ public static byte[] buildPsshAtom(UUID uuid, byte[] data) {
+ int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length;
+ ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength);
+ psshBox.putInt(psshBoxLength);
+ psshBox.putInt(Atom.TYPE_pssh);
+ psshBox.putInt(0 /* version=0, flags=0 */);
+ psshBox.putLong(uuid.getMostSignificantBits());
+ psshBox.putLong(uuid.getLeastSignificantBits());
+ psshBox.putInt(data.length);
+ psshBox.put(data);
+ return psshBox.array();
+ }
+
+ /**
+ * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ * <p>
+ * The UUID is only parsed if the data is a valid PSSH atom.
+ *
+ * @param atom The atom to parse.
+ * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has
+ * an unsupported version.
+ */
+ public static UUID parseUuid(byte[] atom) {
+ Pair<UUID, byte[]> parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return null;
+ }
+ return parsedAtom.first;
+ }
+
+ /**
+ * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
+ * <p>
+ * The scheme specific data is only parsed if the data is a valid PSSH atom matching the given
+ * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null.
+ *
+ * @param atom The atom to parse.
+ * @param uuid The required UUID of the PSSH atom, or null to accept any UUID.
+ * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the
+ * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID.
+ */
+ public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {
+ Pair<UUID, byte[]> parsedAtom = parsePsshAtom(atom);
+ if (parsedAtom == null) {
+ return null;
+ }
+ if (uuid != null && !uuid.equals(parsedAtom.first)) {
+ Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.first + ".");
+ return null;
+ }
+ return parsedAtom.second;
+ }
+
+ /**
+ * Parses the UUID and scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are
+ * supported.
+ *
+ * @param atom The atom to parse.
+ * @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is
+ * not a valid PSSH atom, or if the PSSH atom has an unsupported version.
+ */
+ private static Pair<UUID, byte[]> parsePsshAtom(byte[] atom) {
+ ParsableByteArray atomData = new ParsableByteArray(atom);
+ if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) {
+ // Data too short.
+ return null;
+ }
+ atomData.setPosition(0);
+ int atomSize = atomData.readInt();
+ if (atomSize != atomData.bytesLeft() + 4) {
+ // Not an atom, or incorrect atom size.
+ return null;
+ }
+ int atomType = atomData.readInt();
+ if (atomType != Atom.TYPE_pssh) {
+ // Not an atom, or incorrect atom type.
+ return null;
+ }
+ int atomVersion = Atom.parseFullAtomVersion(atomData.readInt());
+ if (atomVersion > 1) {
+ Log.w(TAG, "Unsupported pssh version: " + atomVersion);
+ return null;
+ }
+ UUID uuid = new UUID(atomData.readLong(), atomData.readLong());
+ if (atomVersion == 1) {
+ int keyIdCount = atomData.readUnsignedIntToInt();
+ atomData.skipBytes(16 * keyIdCount);
+ }
+ int dataSize = atomData.readUnsignedIntToInt();
+ if (dataSize != atomData.bytesLeft()) {
+ // Incorrect dataSize.
+ return null;
+ }
+ byte[] data = new byte[dataSize];
+ atomData.readBytes(data, 0, dataSize);
+ return Pair.create(uuid, data);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
@@ -0,0 +1,187 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Provides methods that peek data from an {@link ExtractorInput} and return whether the input
+ * appears to be in MP4 format.
+ */
+/* package */ final class Sniffer {
+
+ /**
+ * The maximum number of bytes to peek when sniffing.
+ */
+ private static final int SEARCH_LENGTH = 4 * 1024;
+
+ private static final int[] COMPATIBLE_BRANDS = new int[] {
+ Util.getIntegerCodeForString("isom"),
+ Util.getIntegerCodeForString("iso2"),
+ Util.getIntegerCodeForString("iso3"),
+ Util.getIntegerCodeForString("iso4"),
+ Util.getIntegerCodeForString("iso5"),
+ Util.getIntegerCodeForString("iso6"),
+ Util.getIntegerCodeForString("avc1"),
+ Util.getIntegerCodeForString("hvc1"),
+ Util.getIntegerCodeForString("hev1"),
+ Util.getIntegerCodeForString("mp41"),
+ Util.getIntegerCodeForString("mp42"),
+ Util.getIntegerCodeForString("3g2a"),
+ Util.getIntegerCodeForString("3g2b"),
+ Util.getIntegerCodeForString("3gr6"),
+ Util.getIntegerCodeForString("3gs6"),
+ Util.getIntegerCodeForString("3ge6"),
+ Util.getIntegerCodeForString("3gg6"),
+ Util.getIntegerCodeForString("M4V "),
+ Util.getIntegerCodeForString("M4A "),
+ Util.getIntegerCodeForString("f4v "),
+ Util.getIntegerCodeForString("kddi"),
+ Util.getIntegerCodeForString("M4VP"),
+ Util.getIntegerCodeForString("qt "), // Apple QuickTime
+ Util.getIntegerCodeForString("MSNV"), // Sony PSP
+ };
+
+ /**
+ * Returns whether data peeked from the current position in {@code input} is consistent with the
+ * input being a fragmented MP4 file.
+ *
+ * @param input The extractor input from which to peek data. The peek position will be modified.
+ * @return Whether the input appears to be in the fragmented MP4 format.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static boolean sniffFragmented(ExtractorInput input)
+ throws IOException, InterruptedException {
+ return sniffInternal(input, true);
+ }
+
+ /**
+ * Returns whether data peeked from the current position in {@code input} is consistent with the
+ * input being an unfragmented MP4 file.
+ *
+ * @param input The extractor input from which to peek data. The peek position will be modified.
+ * @return Whether the input appears to be in the unfragmented MP4 format.
+ * @throws IOException If an error occurs reading from the input.
+ * @throws InterruptedException If the thread has been interrupted.
+ */
+ public static boolean sniffUnfragmented(ExtractorInput input)
+ throws IOException, InterruptedException {
+ return sniffInternal(input, false);
+ }
+
+ private static boolean sniffInternal(ExtractorInput input, boolean fragmented)
+ throws IOException, InterruptedException {
+ long inputLength = input.getLength();
+ int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
+ ? SEARCH_LENGTH : inputLength);
+
+ ParsableByteArray buffer = new ParsableByteArray(64);
+ int bytesSearched = 0;
+ boolean foundGoodFileType = false;
+ boolean isFragmented = false;
+ while (bytesSearched < bytesToSearch) {
+ // Read an atom header.
+ int headerSize = Atom.HEADER_SIZE;
+ buffer.reset(headerSize);
+ input.peekFully(buffer.data, 0, headerSize);
+ long atomSize = buffer.readUnsignedInt();
+ int atomType = buffer.readInt();
+ if (atomSize == Atom.LONG_SIZE_PREFIX) {
+ headerSize = Atom.LONG_HEADER_SIZE;
+ input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
+ buffer.setLimit(Atom.LONG_HEADER_SIZE);
+ atomSize = buffer.readUnsignedLongToLong();
+ }
+
+ if (atomSize < headerSize) {
+ // The file is invalid because the atom size is too small for its header.
+ return false;
+ }
+ bytesSearched += headerSize;
+
+ if (atomType == Atom.TYPE_moov) {
+ // Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
+ continue;
+ }
+
+ if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) {
+ // The movie is fragmented. Stop searching as we must have read any ftyp atom already.
+ isFragmented = true;
+ break;
+ }
+
+ if (bytesSearched + atomSize - headerSize >= bytesToSearch) {
+ // Stop searching as peeking this atom would exceed the search limit.
+ break;
+ }
+
+ int atomDataSize = (int) (atomSize - headerSize);
+ bytesSearched += atomDataSize;
+ if (atomType == Atom.TYPE_ftyp) {
+ // Parse the atom and check the file type/brand is compatible with the extractors.
+ if (atomDataSize < 8) {
+ return false;
+ }
+ buffer.reset(atomDataSize);
+ input.peekFully(buffer.data, 0, atomDataSize);
+ int brandsCount = atomDataSize / 4;
+ for (int i = 0; i < brandsCount; i++) {
+ if (i == 1) {
+ // This index refers to the minorVersion, not a brand, so skip it.
+ buffer.skipBytes(4);
+ } else if (isCompatibleBrand(buffer.readInt())) {
+ foundGoodFileType = true;
+ break;
+ }
+ }
+ if (!foundGoodFileType) {
+ // The types were not compatible and there is only one ftyp atom, so reject the file.
+ return false;
+ }
+ } else if (atomDataSize != 0) {
+ // Skip the atom.
+ input.advancePeekPosition(atomDataSize);
+ }
+ }
+ return foundGoodFileType && fragmented == isFragmented;
+ }
+
+ /**
+ * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors.
+ */
+ private static boolean isCompatibleBrand(int brand) {
+ // Accept all brands starting '3gp'.
+ if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) {
+ return true;
+ }
+ for (int compatibleBrand : COMPATIBLE_BRANDS) {
+ if (compatibleBrand == brand) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Sniffer() {
+ // Prevent instantiation.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/Track.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Encapsulates information describing an MP4 track.
+ */
+public final class Track {
+
+ /**
+ * The transformation to apply to samples in the track, if any.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT})
+ public @interface Transformation {}
+ /**
+ * A no-op sample transformation.
+ */
+ public static final int TRANSFORMATION_NONE = 0;
+ /**
+ * A transformation for caption samples in cdat atoms.
+ */
+ public static final int TRANSFORMATION_CEA608_CDAT = 1;
+
+ /**
+ * The track identifier.
+ */
+ public final int id;
+
+ /**
+ * One of {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}.
+ */
+ public final int type;
+
+ /**
+ * The track timescale, defined as the number of time units that pass in one second.
+ */
+ public final long timescale;
+
+ /**
+ * The movie timescale.
+ */
+ public final long movieTimescale;
+
+ /**
+ * The duration of the track in microseconds, or {@link C#TIME_UNSET} if unknown.
+ */
+ public final long durationUs;
+
+ /**
+ * The format.
+ */
+ public final Format format;
+
+ /**
+ * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each
+ * sample.
+ */
+ @Transformation public final int sampleTransformation;
+
+ /**
+ * Track encryption boxes for the different track sample descriptions. Entries may be null.
+ */
+ public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
+
+ /**
+ * Durations of edit list segments in the movie timescale. Null if there is no edit list.
+ */
+ public final long[] editListDurations;
+
+ /**
+ * Media times for edit list segments in the track timescale. Null if there is no edit list.
+ */
+ public final long[] editListMediaTimes;
+
+ /**
+ * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for
+ * other track types.
+ */
+ public final int nalUnitLengthFieldLength;
+
+ public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
+ Format format, @Transformation int sampleTransformation,
+ TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
+ long[] editListDurations, long[] editListMediaTimes) {
+ this.id = id;
+ this.type = type;
+ this.timescale = timescale;
+ this.movieTimescale = movieTimescale;
+ this.durationUs = durationUs;
+ this.format = format;
+ this.sampleTransformation = sampleTransformation;
+ this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ this.editListDurations = editListDurations;
+ this.editListMediaTimes = editListMediaTimes;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
@@ -0,0 +1,51 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+/**
+ * Encapsulates information parsed from a track encryption (tenc) box or sample group description
+ * (sgpd) box in an MP4 stream.
+ */
+public final class TrackEncryptionBox {
+
+ /**
+ * Indicates the encryption state of the samples in the sample group.
+ */
+ public final boolean isEncrypted;
+
+ /**
+ * The initialization vector size in bytes for the samples in the corresponding sample group.
+ */
+ public final int initializationVectorSize;
+
+ /**
+ * The key identifier for the samples in the corresponding sample group.
+ */
+ public final byte[] keyId;
+
+ /**
+ * @param isEncrypted Indicates the encryption state of the samples in the sample group.
+ * @param initializationVectorSize The initialization vector size in bytes for the samples in the
+ * corresponding sample group.
+ * @param keyId The key identifier for the samples in the corresponding sample group.
+ */
+ public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) {
+ this.isEncrypted = isEncrypted;
+ this.initializationVectorSize = initializationVectorSize;
+ this.keyId = keyId;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
@@ -0,0 +1,193 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * A holder for information corresponding to a single fragment of an mp4 file.
+ */
+/* package */ final class TrackFragment {
+
+ /**
+ * The default values for samples from the track fragment header.
+ */
+ public DefaultSampleValues header;
+ /**
+ * The position (byte offset) of the start of fragment.
+ */
+ public long atomPosition;
+ /**
+ * The position (byte offset) of the start of data contained in the fragment.
+ */
+ public long dataPosition;
+ /**
+ * The position (byte offset) of the start of auxiliary data.
+ */
+ public long auxiliaryDataPosition;
+ /**
+ * The number of track runs of the fragment.
+ */
+ public int trunCount;
+ /**
+ * The total number of samples in the fragment.
+ */
+ public int sampleCount;
+ /**
+ * The position (byte offset) of the start of sample data of each track run in the fragment.
+ */
+ public long[] trunDataPosition;
+ /**
+ * The number of samples contained by each track run in the fragment.
+ */
+ public int[] trunLength;
+ /**
+ * The size of each sample in the fragment.
+ */
+ public int[] sampleSizeTable;
+ /**
+ * The composition time offset of each sample in the fragment.
+ */
+ public int[] sampleCompositionTimeOffsetTable;
+ /**
+ * The decoding time of each sample in the fragment.
+ */
+ public long[] sampleDecodingTimeTable;
+ /**
+ * Indicates which samples are sync frames.
+ */
+ public boolean[] sampleIsSyncFrameTable;
+ /**
+ * Whether the fragment defines encryption data.
+ */
+ public boolean definesEncryptionData;
+ /**
+ * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.
+ * Undefined otherwise.
+ */
+ public boolean[] sampleHasSubsampleEncryptionTable;
+ /**
+ * Fragment specific track encryption. May be null.
+ */
+ public TrackEncryptionBox trackEncryptionBox;
+ /**
+ * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.
+ * Undefined otherwise.
+ */
+ public int sampleEncryptionDataLength;
+ /**
+ * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined
+ * otherwise.
+ */
+ public ParsableByteArray sampleEncryptionData;
+ /**
+ * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.
+ */
+ public boolean sampleEncryptionDataNeedsFill;
+ /**
+ * The absolute decode time of the start of the next fragment.
+ */
+ public long nextFragmentDecodeTime;
+
+ /**
+ * Resets the fragment.
+ * <p>
+ * {@link #sampleCount} and {@link #nextFragmentDecodeTime} are set to 0, and both
+ * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false,
+ * and {@link #trackEncryptionBox} is set to null.
+ */
+ public void reset() {
+ trunCount = 0;
+ nextFragmentDecodeTime = 0;
+ definesEncryptionData = false;
+ sampleEncryptionDataNeedsFill = false;
+ trackEncryptionBox = null;
+ }
+
+ /**
+ * Configures the fragment for the specified number of samples.
+ * <p>
+ * The {@link #sampleCount} of the fragment is set to the specified sample count, and the
+ * contained tables are resized if necessary such that they are at least this length.
+ *
+ * @param sampleCount The number of samples in the new run.
+ */
+ public void initTables(int trunCount, int sampleCount) {
+ this.trunCount = trunCount;
+ this.sampleCount = sampleCount;
+ if (trunLength == null || trunLength.length < trunCount) {
+ trunDataPosition = new long[trunCount];
+ trunLength = new int[trunCount];
+ }
+ if (sampleSizeTable == null || sampleSizeTable.length < sampleCount) {
+ // Size the tables 25% larger than needed, so as to make future resize operations less
+ // likely. The choice of 25% is relatively arbitrary.
+ int tableSize = (sampleCount * 125) / 100;
+ sampleSizeTable = new int[tableSize];
+ sampleCompositionTimeOffsetTable = new int[tableSize];
+ sampleDecodingTimeTable = new long[tableSize];
+ sampleIsSyncFrameTable = new boolean[tableSize];
+ sampleHasSubsampleEncryptionTable = new boolean[tableSize];
+ }
+ }
+
+ /**
+ * Configures the fragment to be one that defines encryption data of the specified length.
+ * <p>
+ * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to
+ * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it
+ * is at least this length.
+ *
+ * @param length The length in bytes of the encryption data.
+ */
+ public void initEncryptionData(int length) {
+ if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) {
+ sampleEncryptionData = new ParsableByteArray(length);
+ }
+ sampleEncryptionDataLength = length;
+ definesEncryptionData = true;
+ sampleEncryptionDataNeedsFill = true;
+ }
+
+ /**
+ * Fills {@link #sampleEncryptionData} from the provided input.
+ *
+ * @param input An {@link ExtractorInput} from which to read the encryption data.
+ */
+ public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
+ input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+ sampleEncryptionData.setPosition(0);
+ sampleEncryptionDataNeedsFill = false;
+ }
+
+ /**
+ * Fills {@link #sampleEncryptionData} from the provided source.
+ *
+ * @param source A source from which to read the encryption data.
+ */
+ public void fillEncryptionData(ParsableByteArray source) {
+ source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+ sampleEncryptionData.setPosition(0);
+ sampleEncryptionDataNeedsFill = false;
+ }
+
+ public long getSamplePresentationTime(int index) {
+ return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Sample table for a track in an MP4 file.
+ */
+/* package */ final class TrackSampleTable {
+
+ /**
+ * Number of samples.
+ */
+ public final int sampleCount;
+ /**
+ * Sample offsets in bytes.
+ */
+ public final long[] offsets;
+ /**
+ * Sample sizes in bytes.
+ */
+ public final int[] sizes;
+ /**
+ * Maximum sample size in {@link #sizes}.
+ */
+ public final int maximumSize;
+ /**
+ * Sample timestamps in microseconds.
+ */
+ public final long[] timestampsUs;
+ /**
+ * Sample flags.
+ */
+ public final int[] flags;
+
+ public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs,
+ int[] flags) {
+ Assertions.checkArgument(sizes.length == timestampsUs.length);
+ Assertions.checkArgument(offsets.length == timestampsUs.length);
+ Assertions.checkArgument(flags.length == timestampsUs.length);
+
+ this.offsets = offsets;
+ this.sizes = sizes;
+ this.maximumSize = maximumSize;
+ this.timestampsUs = timestampsUs;
+ this.flags = flags;
+ sampleCount = offsets.length;
+ }
+
+ /**
+ * Returns the sample index of the closest synchronization sample at or before the given
+ * timestamp, if one is available.
+ *
+ * @param timeUs Timestamp adjacent to which to find a synchronization sample.
+ * @return Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.
+ */
+ public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) {
+ // Video frame timestamps may not be sorted, so the behavior of this call can be undefined.
+ // Frames are not reordered past synchronization samples so this works in practice.
+ int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false);
+ for (int i = startIndex; i >= 0; i--) {
+ if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the sample index of the closest synchronization sample at or after the given timestamp,
+ * if one is available.
+ *
+ * @param timeUs Timestamp adjacent to which to find a synchronization sample.
+ * @return index Index of the synchronization sample, or {@link C#INDEX_UNSET} if none.
+ */
+ public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) {
+ int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false);
+ for (int i = startIndex; i < timestampsUs.length; i++) {
+ if ((flags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
@@ -0,0 +1,340 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream.
+ */
+/* package */ final class DefaultOggSeeker implements OggSeeker {
+
+ //@VisibleForTesting
+ public static final int MATCH_RANGE = 72000;
+ //@VisibleForTesting
+ public static final int MATCH_BYTE_RANGE = 100000;
+ private static final int DEFAULT_OFFSET = 30000;
+
+ private static final int STATE_SEEK_TO_END = 0;
+ private static final int STATE_READ_LAST_PAGE = 1;
+ private static final int STATE_SEEK = 2;
+ private static final int STATE_IDLE = 3;
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final long startPosition;
+ private final long endPosition;
+ private final StreamReader streamReader;
+
+ private int state;
+ private long totalGranules;
+ private long positionBeforeSeekToEnd;
+ private long targetGranule;
+
+ private long start;
+ private long end;
+ private long startGranule;
+ private long endGranule;
+
+ /**
+ * Constructs an OggSeeker.
+ * @param startPosition Start position of the payload (inclusive).
+ * @param endPosition End position of the payload (exclusive).
+ * @param streamReader StreamReader instance which owns this OggSeeker
+ * @param firstPayloadPageSize The total size of the first payload page, in bytes.
+ * @param firstPayloadPageGranulePosition The granule position of the first payload page.
+ */
+ public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader,
+ int firstPayloadPageSize, long firstPayloadPageGranulePosition) {
+ Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition);
+ this.streamReader = streamReader;
+ this.startPosition = startPosition;
+ this.endPosition = endPosition;
+ if (firstPayloadPageSize == endPosition - startPosition) {
+ totalGranules = firstPayloadPageGranulePosition;
+ state = STATE_IDLE;
+ } else {
+ state = STATE_SEEK_TO_END;
+ }
+ }
+
+ @Override
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_IDLE:
+ return -1;
+ case STATE_SEEK_TO_END:
+ positionBeforeSeekToEnd = input.getPosition();
+ state = STATE_READ_LAST_PAGE;
+ // Seek to the end just before the last page of stream to get the duration.
+ long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE;
+ if (lastPageSearchPosition > positionBeforeSeekToEnd) {
+ return lastPageSearchPosition;
+ }
+ // Fall through.
+ case STATE_READ_LAST_PAGE:
+ totalGranules = readGranuleOfLastPage(input);
+ state = STATE_IDLE;
+ return positionBeforeSeekToEnd;
+ case STATE_SEEK:
+ long currentGranule;
+ if (targetGranule == 0) {
+ currentGranule = 0;
+ } else {
+ long position = getNextSeekPosition(targetGranule, input);
+ if (position >= 0) {
+ return position;
+ }
+ currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2));
+ }
+ state = STATE_IDLE;
+ return -(currentGranule + 2);
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public long startSeek(long timeUs) {
+ Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK);
+ targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs);
+ state = STATE_SEEK;
+ resetSeeking();
+ return targetGranule;
+ }
+
+ @Override
+ public OggSeekMap createSeekMap() {
+ return totalGranules != 0 ? new OggSeekMap() : null;
+ }
+
+ //@VisibleForTesting
+ public void resetSeeking() {
+ start = startPosition;
+ end = endPosition;
+ startGranule = 0;
+ endGranule = totalGranules;
+ }
+
+ /**
+ * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
+ * has to seek and then be passed for another call until a negative number is returned. If a
+ * negative number is returned the input is at a position which is before the target page and at
+ * which it is sensible to just skip pages to the target granule and pre-roll instead of doing
+ * another seek request.
+ *
+ * @param targetGranule the target granule position to seek to.
+ * @param input the {@link ExtractorInput} to read from.
+ * @return the position to seek the {@link ExtractorInput} to for a next call or
+ * -(currentGranule + 2) if it's close enough to skip to the target page.
+ * @throws IOException thrown if reading from the input fails.
+ * @throws InterruptedException thrown if interrupted while reading from the input.
+ */
+ //@VisibleForTesting
+ public long getNextSeekPosition(long targetGranule, ExtractorInput input)
+ throws IOException, InterruptedException {
+ if (start == end) {
+ return -(startGranule + 2);
+ }
+
+ long initialPosition = input.getPosition();
+ if (!skipToNextPage(input, end)) {
+ if (start == initialPosition) {
+ throw new IOException("No ogg page can be found.");
+ }
+ return start;
+ }
+
+ pageHeader.populate(input, false);
+ input.resetPeekPosition();
+
+ long granuleDistance = targetGranule - pageHeader.granulePosition;
+ int pageSize = pageHeader.headerSize + pageHeader.bodySize;
+ if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) {
+ if (granuleDistance < 0) {
+ end = initialPosition;
+ endGranule = pageHeader.granulePosition;
+ } else {
+ start = input.getPosition() + pageSize;
+ startGranule = pageHeader.granulePosition;
+ if (end - start + pageSize < MATCH_BYTE_RANGE) {
+ input.skipFully(pageSize);
+ return -(startGranule + 2);
+ }
+ }
+
+ if (end - start < MATCH_BYTE_RANGE) {
+ end = start;
+ return start;
+ }
+
+ long offset = pageSize * (granuleDistance <= 0 ? 2 : 1);
+ long nextPosition = input.getPosition() - offset
+ + (granuleDistance * (end - start) / (endGranule - startGranule));
+
+ nextPosition = Math.max(nextPosition, start);
+ nextPosition = Math.min(nextPosition, end - 1);
+ return nextPosition;
+ }
+
+ // position accepted (before target granule and within MATCH_RANGE)
+ input.skipFully(pageSize);
+ return -(pageHeader.granulePosition + 2);
+ }
+
+ private long getEstimatedPosition(long position, long granuleDistance, long offset) {
+ position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset;
+ if (position < startPosition) {
+ position = startPosition;
+ }
+ if (position >= endPosition) {
+ position = endPosition - 1;
+ }
+ return position;
+ }
+
+ private class OggSeekMap implements SeekMap {
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ if (timeUs == 0) {
+ return startPosition;
+ }
+ long granule = streamReader.convertTimeToGranule(timeUs);
+ return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
+ }
+
+ @Override
+ public long getDurationUs() {
+ return streamReader.convertGranuleToTime(totalGranules);
+ }
+
+ }
+
+ /**
+ * Skips to the next page.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @throws IOException thrown if peeking/reading from the input fails.
+ * @throws InterruptedException thrown if interrupted while peeking/reading from the input.
+ * @throws EOFException if the next page can't be found before the end of the input.
+ */
+ //@VisibleForTesting
+ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
+ if (!skipToNextPage(input, endPosition)) {
+ // Not found until eof.
+ throw new EOFException();
+ }
+ }
+
+ /**
+ * Skips to the next page. Searches for the next page header.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @param until Searches until this position.
+ * @return true if the next page is found.
+ * @throws IOException thrown if peeking/reading from the input fails.
+ * @throws InterruptedException thrown if interrupted while peeking/reading from the input.
+ */
+ //@VisibleForTesting
+ boolean skipToNextPage(ExtractorInput input, long until)
+ throws IOException, InterruptedException {
+ until = Math.min(until + 3, endPosition);
+ byte[] buffer = new byte[2048];
+ int peekLength = buffer.length;
+ while (true) {
+ if (input.getPosition() + peekLength > until) {
+ // Make sure to not peek beyond the end of the input.
+ peekLength = (int) (until - input.getPosition());
+ if (peekLength < 4) {
+ // Not found until end.
+ return false;
+ }
+ }
+ input.peekFully(buffer, 0, peekLength, false);
+ for (int i = 0; i < peekLength - 3; i++) {
+ if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g'
+ && buffer[i + 3] == 'S') {
+ // Match! Skip to the start of the pattern.
+ input.skipFully(i);
+ return true;
+ }
+ }
+ // Overlap by not skipping the entire peekLength.
+ input.skipFully(peekLength - 3);
+ }
+ }
+
+ /**
+ * Skips to the last Ogg page in the stream and reads the header's granule field which is the
+ * total number of samples per channel.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return the total number of samples of this input.
+ * @throws IOException thrown if reading from the input fails.
+ * @throws InterruptedException thrown if interrupted while reading from the input.
+ */
+ //@VisibleForTesting
+ long readGranuleOfLastPage(ExtractorInput input)
+ throws IOException, InterruptedException {
+ skipToNextPage(input);
+ pageHeader.reset();
+ while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) {
+ pageHeader.populate(input, false);
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ }
+ return pageHeader.granulePosition;
+ }
+
+ /**
+ * Skips to the position of the start of the page containing the {@code targetGranule} and
+ * returns the granule of the page previous to the target page.
+ *
+ * @param input the {@link ExtractorInput} to read from.
+ * @param targetGranule the target granule.
+ * @param currentGranule the current granule or -1 if it's unknown.
+ * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior
+ * page.
+ * @throws ParserException thrown if populating the page header fails.
+ * @throws IOException thrown if reading from the input fails.
+ * @throws InterruptedException thrown if interrupted while reading from the input.
+ */
+ //@VisibleForTesting
+ long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule)
+ throws IOException, InterruptedException {
+ pageHeader.populate(input, false);
+ while (pageHeader.granulePosition < targetGranule) {
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ // Store in a member field to be able to resume after IOExceptions.
+ currentGranule = pageHeader.granulePosition;
+ // Peek next header.
+ pageHeader.populate(input, false);
+ }
+ input.resetPeekPosition();
+ return currentGranule;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
@@ -0,0 +1,207 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link StreamReader} to extract Flac data out of Ogg byte stream.
+ */
+/* package */ final class FlacReader extends StreamReader {
+
+ private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
+ private static final byte SEEKTABLE_PACKET_TYPE = 0x03;
+
+ private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
+
+ private FlacStreamInfo streamInfo;
+ private FlacOggSeeker flacOggSeeker;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
+ data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ streamInfo = null;
+ flacOggSeeker = null;
+ }
+ }
+
+ private static boolean isAudioPacket(byte[] data) {
+ return data[0] == AUDIO_PACKET_TYPE;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ if (!isAudioPacket(packet.data)) {
+ return -1;
+ }
+ return getFlacFrameBlockSize(packet);
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
+ throws IOException, InterruptedException {
+ byte[] data = packet.data;
+ if (streamInfo == null) {
+ streamInfo = new FlacStreamInfo(data, 17);
+ byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
+ metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
+ List<byte[]> initializationData = Collections.singletonList(metadata);
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null,
+ Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
+ initializationData, null, 0, null);
+ } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
+ flacOggSeeker = new FlacOggSeeker();
+ flacOggSeeker.parseSeekTable(packet);
+ } else if (isAudioPacket(data)) {
+ if (flacOggSeeker != null) {
+ flacOggSeeker.setFirstFrameOffset(position);
+ setupData.oggSeeker = flacOggSeeker;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private int getFlacFrameBlockSize(ParsableByteArray packet) {
+ int blockSizeCode = (packet.data[2] & 0xFF) >> 4;
+ switch (blockSizeCode) {
+ case 1:
+ return 192;
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ return 576 << (blockSizeCode - 2);
+ case 6:
+ case 7:
+ // skip the sample number
+ packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
+ packet.readUtf8EncodedLong();
+ int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort();
+ packet.setPosition(0);
+ return value + 1;
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ return 256 << (blockSizeCode - 8);
+ }
+ return -1;
+ }
+
+ private class FlacOggSeeker implements OggSeeker, SeekMap {
+
+ private static final int METADATA_LENGTH_OFFSET = 1;
+ private static final int SEEK_POINT_SIZE = 18;
+
+ private long[] seekPointGranules;
+ private long[] seekPointOffsets;
+ private long firstFrameOffset;
+ private long pendingSeekGranule;
+
+ public FlacOggSeeker() {
+ firstFrameOffset = -1;
+ pendingSeekGranule = -1;
+ }
+
+ public void setFirstFrameOffset(long firstFrameOffset) {
+ this.firstFrameOffset = firstFrameOffset;
+ }
+
+ /**
+ * Parses a FLAC file seek table metadata structure and initializes internal fields.
+ *
+ * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its
+ * position should be set to the beginning of the block.
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
+ * METADATA_BLOCK_SEEKTABLE</a>
+ */
+ public void parseSeekTable(ParsableByteArray data) {
+ data.skipBytes(METADATA_LENGTH_OFFSET);
+ int length = data.readUnsignedInt24();
+ int numberOfSeekPoints = length / SEEK_POINT_SIZE;
+ seekPointGranules = new long[numberOfSeekPoints];
+ seekPointOffsets = new long[numberOfSeekPoints];
+ for (int i = 0; i < numberOfSeekPoints; i++) {
+ seekPointGranules[i] = data.readLong();
+ seekPointOffsets[i] = data.readLong();
+ data.skipBytes(2); // Skip "Number of samples in the target frame."
+ }
+ }
+
+ @Override
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ if (pendingSeekGranule >= 0) {
+ long result = -(pendingSeekGranule + 2);
+ pendingSeekGranule = -1;
+ return result;
+ }
+ return -1;
+ }
+
+ @Override
+ public long startSeek(long timeUs) {
+ long granule = convertTimeToGranule(timeUs);
+ int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
+ pendingSeekGranule = seekPointGranules[index];
+ return granule;
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ return this;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ long granule = convertTimeToGranule(timeUs);
+ int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
+ return firstFrameOffset + seekPointOffsets[index];
+ }
+
+ @Override
+ public long getDurationUs() {
+ return streamInfo.durationUs();
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -0,0 +1,111 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Ogg {@link Extractor}.
+ */
+public class OggExtractor implements Extractor {
+
+ /**
+ * Factory for {@link OggExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new OggExtractor()};
+ }
+
+ };
+
+ private static final int MAX_VERIFICATION_BYTES = 8;
+
+ private StreamReader streamReader;
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ try {
+ OggPageHeader header = new OggPageHeader();
+ if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
+ return false;
+ }
+
+ int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.peekFully(scratch.data, 0, length);
+
+ if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new FlacReader();
+ } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new VorbisReader();
+ } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new OpusReader();
+ } else {
+ return false;
+ }
+ return true;
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ // TODO: fix the case if sniff() isn't called
+ streamReader.init(output, trackOutput);
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ streamReader.seek(position, timeUs);
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ return streamReader.read(input, seekPosition);
+ }
+
+ //@VisibleForTesting
+ /* package */ StreamReader getStreamReader() {
+ return streamReader;
+ }
+
+ private static ParsableByteArray resetPosition(ParsableByteArray scratch) {
+ scratch.setPosition(0);
+ return scratch;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
@@ -0,0 +1,141 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * OGG packet class.
+ */
+/* package */ final class OggPacket {
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final ParsableByteArray packetArray =
+ new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
+
+ private int currentSegmentIndex = C.INDEX_UNSET;
+ private int segmentCount;
+ private boolean populated;
+
+ /**
+ * Resets this reader.
+ */
+ public void reset() {
+ pageHeader.reset();
+ packetArray.reset();
+ currentSegmentIndex = C.INDEX_UNSET;
+ populated = false;
+ }
+
+ /**
+ * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
+ * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
+ * can resume properly from an error while reading a continued packet spanned across multiple
+ * pages.
+ *
+ * @param input the {@link ExtractorInput} to read data from.
+ * @return {@code true} if the read was successful. {@code false} if the end of the input was
+ * encountered having read no data.
+ * @throws IOException thrown if reading from the input fails.
+ * @throws InterruptedException thrown if interrupted while reading from input.
+ */
+ public boolean populate(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkState(input != null);
+
+ if (populated) {
+ populated = false;
+ packetArray.reset();
+ }
+
+ while (!populated) {
+ if (currentSegmentIndex < 0) {
+ // We're at the start of a page.
+ if (!pageHeader.populate(input, true)) {
+ return false;
+ }
+ int segmentIndex = 0;
+ int bytesToSkip = pageHeader.headerSize;
+ if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
+ // After seeking, the first packet may be the remainder
+ // part of a continued packet which has to be discarded.
+ bytesToSkip += calculatePacketSize(segmentIndex);
+ segmentIndex += segmentCount;
+ }
+ input.skipFully(bytesToSkip);
+ currentSegmentIndex = segmentIndex;
+ }
+
+ int size = calculatePacketSize(currentSegmentIndex);
+ int segmentIndex = currentSegmentIndex + segmentCount;
+ if (size > 0) {
+ input.readFully(packetArray.data, packetArray.limit(), size);
+ packetArray.setLimit(packetArray.limit() + size);
+ populated = pageHeader.laces[segmentIndex - 1] != 255;
+ }
+ // Advance now since we are sure reading didn't throw an exception.
+ currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET
+ : segmentIndex;
+ }
+ return true;
+ }
+
+ /**
+ * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read,
+ * or an empty header if the packet has yet to be populated.
+ * <p>
+ * Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent
+ * calls to {@link #populate(ExtractorInput)}.
+ *
+ * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet
+ * to be populated.
+ */
+ //@VisibleForTesting
+ public OggPageHeader getPageHeader() {
+ return pageHeader;
+ }
+
+ /**
+ * Returns a {@link ParsableByteArray} containing the packet's payload.
+ */
+ public ParsableByteArray getPayload() {
+ return packetArray;
+ }
+
+ /**
+ * Calculates the size of the packet starting from {@code startSegmentIndex}.
+ *
+ * @param startSegmentIndex the index of the first segment of the packet.
+ * @return Size of the packet.
+ */
+ private int calculatePacketSize(int startSegmentIndex) {
+ segmentCount = 0;
+ int size = 0;
+ while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {
+ int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];
+ size += segmentLength;
+ if (segmentLength != 255) {
+ // packets end at first lace < 255
+ break;
+ }
+ }
+ return size;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
@@ -0,0 +1,130 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Data object to store header information.
+ */
+/* package */ final class OggPageHeader {
+
+ public static final int EMPTY_PAGE_HEADER_SIZE = 27;
+ public static final int MAX_SEGMENT_COUNT = 255;
+ public static final int MAX_PAGE_PAYLOAD = 255 * 255;
+ public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT
+ + MAX_PAGE_PAYLOAD;
+
+ private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS");
+
+ public int revision;
+ public int type;
+ public long granulePosition;
+ public long streamSerialNumber;
+ public long pageSequenceNumber;
+ public long pageChecksum;
+ public int pageSegmentCount;
+ public int headerSize;
+ public int bodySize;
+ /**
+ * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use
+ * {@link #pageSegmentCount} to iterate.
+ */
+ public final int[] laces = new int[MAX_SEGMENT_COUNT];
+
+ private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT);
+
+ /**
+ * Resets all primitive member fields to zero.
+ */
+ public void reset() {
+ revision = 0;
+ type = 0;
+ granulePosition = 0;
+ streamSerialNumber = 0;
+ pageSequenceNumber = 0;
+ pageChecksum = 0;
+ pageSegmentCount = 0;
+ headerSize = 0;
+ bodySize = 0;
+ }
+
+ /**
+ * Peeks an Ogg page header and updates this {@link OggPageHeader}.
+ *
+ * @param input the {@link ExtractorInput} to read from.
+ * @param quiet if {@code true} no Exceptions are thrown but {@code false} is return if something
+ * goes wrong.
+ * @return {@code true} if the read was successful. {@code false} if the end of the input was
+ * encountered having read no data.
+ * @throws IOException thrown if reading data fails or the stream is invalid.
+ * @throws InterruptedException thrown if thread is interrupted when reading/peeking.
+ */
+ public boolean populate(ExtractorInput input, boolean quiet)
+ throws IOException, InterruptedException {
+ scratch.reset();
+ reset();
+ boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET
+ || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE;
+ if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new EOFException();
+ }
+ }
+ if (scratch.readUnsignedInt() != TYPE_OGGS) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected OggS capture pattern at begin of page");
+ }
+ }
+
+ revision = scratch.readUnsignedByte();
+ if (revision != 0x00) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("unsupported bit stream revision");
+ }
+ }
+ type = scratch.readUnsignedByte();
+
+ granulePosition = scratch.readLittleEndianLong();
+ streamSerialNumber = scratch.readLittleEndianUnsignedInt();
+ pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
+ pageChecksum = scratch.readLittleEndianUnsignedInt();
+ pageSegmentCount = scratch.readUnsignedByte();
+ headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount;
+
+ // calculate total size of header including laces
+ scratch.reset();
+ input.peekFully(scratch.data, 0, pageSegmentCount);
+ for (int i = 0; i < pageSegmentCount; i++) {
+ laces[i] = scratch.readUnsignedByte();
+ bodySize += laces[i];
+ }
+
+ return true;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive
+ * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position
+ * and start the seeking with an initial estimated position.
+ */
+/* package */ interface OggSeeker {
+
+ /**
+ * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking
+ * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1.
+ */
+ SeekMap createSeekMap();
+
+ /**
+ * Initializes a seek operation.
+ *
+ * @param timeUs The seek position in microseconds.
+ * @return The granule position targeted by the seek.
+ */
+ long startSeek(long timeUs);
+
+ /**
+ * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a
+ * progressive seek.
+ * <p/>
+ * If more data is required or if the position of the input needs to be modified then a position
+ * from which data should be provided is returned. Else a negative value is returned. If a seek
+ * has been completed then the value returned is -(currentGranule + 2). Else it is -1.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2)
+ * if the progressive seek has completed, or -1 otherwise.
+ * @throws IOException If reading from the {@link ExtractorInput} fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ long read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
@@ -0,0 +1,135 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@link StreamReader} to extract Opus data out of Ogg byte stream.
+ */
+/* package */ final class OpusReader extends StreamReader {
+
+ private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
+
+ /**
+ * Opus streams are always decoded at 48000 Hz.
+ */
+ private static final int SAMPLE_RATE = 48000;
+
+ private static final int OPUS_CODE = Util.getIntegerCodeForString("Opus");
+ private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
+
+ private boolean headerRead;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ if (data.bytesLeft() < OPUS_SIGNATURE.length) {
+ return false;
+ }
+ byte[] header = new byte[OPUS_SIGNATURE.length];
+ data.readBytes(header, 0, OPUS_SIGNATURE.length);
+ return Arrays.equals(header, OPUS_SIGNATURE);
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ headerRead = false;
+ }
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ return convertTimeToGranule(getPacketDurationUs(packet.data));
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
+ throws IOException, InterruptedException {
+ if (!headerRead) {
+ byte[] metadata = Arrays.copyOf(packet.data, packet.limit());
+ int channelCount = metadata[9] & 0xFF;
+ int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);
+
+ List<byte[]> initializationData = new ArrayList<>(3);
+ initializationData.add(metadata);
+ putNativeOrderLong(initializationData, preskip);
+ putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0,
+ null);
+ headerRead = true;
+ } else {
+ boolean headerPacket = packet.readInt() == OPUS_CODE;
+ packet.setPosition(0);
+ return headerPacket;
+ }
+ return true;
+ }
+
+ private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
+ long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
+ byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
+ initializationData.add(array);
+ }
+
+ /**
+ * Returns the duration of the given audio packet.
+ *
+ * @param packet Contains audio data.
+ * @return Returns the duration of the given audio packet.
+ */
+ private long getPacketDurationUs(byte[] packet) {
+ int toc = packet[0] & 0xFF;
+ int frames;
+ switch (toc & 0x3) {
+ case 0:
+ frames = 1;
+ break;
+ case 1:
+ case 2:
+ frames = 2;
+ break;
+ default:
+ frames = packet[1] & 0x3F;
+ break;
+ }
+
+ int config = toc >> 3;
+ int length = config & 0x3;
+ if (config >= 16) {
+ length = 2500 << length;
+ } else if (config >= 12) {
+ length = 10000 << (length & 0x1);
+ } else if (length == 3) {
+ length = 60000;
+ } else {
+ length = 10000 << length;
+ }
+ return frames * length;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
@@ -0,0 +1,260 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * StreamReader abstract class.
+ */
+/* package */ abstract class StreamReader {
+
+ private static final int STATE_READ_HEADERS = 0;
+ private static final int STATE_SKIP_HEADERS = 1;
+ private static final int STATE_READ_PAYLOAD = 2;
+ private static final int STATE_END_OF_INPUT = 3;
+
+ static class SetupData {
+ Format format;
+ OggSeeker oggSeeker;
+ }
+
+ private OggPacket oggPacket;
+ private TrackOutput trackOutput;
+ private ExtractorOutput extractorOutput;
+ private OggSeeker oggSeeker;
+ private long targetGranule;
+ private long payloadStartPosition;
+ private long currentGranule;
+ private int state;
+ private int sampleRate;
+ private SetupData setupData;
+ private long lengthOfReadPacket;
+ private boolean seekMapSet;
+ private boolean formatSet;
+
+ void init(ExtractorOutput output, TrackOutput trackOutput) {
+ this.extractorOutput = output;
+ this.trackOutput = trackOutput;
+ this.oggPacket = new OggPacket();
+
+ reset(true);
+ }
+
+ /**
+ * Resets the state of the {@link StreamReader}.
+ *
+ * @param headerData Resets parsed header data too.
+ */
+ protected void reset(boolean headerData) {
+ if (headerData) {
+ setupData = new SetupData();
+ payloadStartPosition = 0;
+ state = STATE_READ_HEADERS;
+ } else {
+ state = STATE_SKIP_HEADERS;
+ }
+ targetGranule = -1;
+ currentGranule = 0;
+ }
+
+ /**
+ * @see Extractor#seek(long, long)
+ */
+ final void seek(long position, long timeUs) {
+ oggPacket.reset();
+ if (position == 0) {
+ reset(!seekMapSet);
+ } else {
+ if (state != STATE_READ_HEADERS) {
+ targetGranule = oggSeeker.startSeek(timeUs);
+ state = STATE_READ_PAYLOAD;
+ }
+ }
+ }
+
+ /**
+ * @see Extractor#read(ExtractorInput, PositionHolder)
+ */
+ final int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_READ_HEADERS:
+ return readHeaders(input);
+
+ case STATE_SKIP_HEADERS:
+ input.skipFully((int) payloadStartPosition);
+ state = STATE_READ_PAYLOAD;
+ return Extractor.RESULT_CONTINUE;
+
+ case STATE_READ_PAYLOAD:
+ return readPayload(input, seekPosition);
+
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ private int readHeaders(ExtractorInput input) throws IOException, InterruptedException {
+ boolean readingHeaders = true;
+ while (readingHeaders) {
+ if (!oggPacket.populate(input)) {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ lengthOfReadPacket = input.getPosition() - payloadStartPosition;
+
+ readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData);
+ if (readingHeaders) {
+ payloadStartPosition = input.getPosition();
+ }
+ }
+
+ sampleRate = setupData.format.sampleRate;
+ if (!formatSet) {
+ trackOutput.format(setupData.format);
+ formatSet = true;
+ }
+
+ if (setupData.oggSeeker != null) {
+ oggSeeker = setupData.oggSeeker;
+ } else if (input.getLength() == C.LENGTH_UNSET) {
+ oggSeeker = new UnseekableOggSeeker();
+ } else {
+ OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader();
+ oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this,
+ firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
+ firstPayloadPageHeader.granulePosition);
+ }
+
+ setupData = null;
+ state = STATE_READ_PAYLOAD;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readPayload(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ long position = oggSeeker.read(input);
+ if (position >= 0) {
+ seekPosition.position = position;
+ return Extractor.RESULT_SEEK;
+ } else if (position < -1) {
+ onSeekEnd(-(position + 2));
+ }
+ if (!seekMapSet) {
+ SeekMap seekMap = oggSeeker.createSeekMap();
+ extractorOutput.seekMap(seekMap);
+ seekMapSet = true;
+ }
+
+ if (lengthOfReadPacket > 0 || oggPacket.populate(input)) {
+ lengthOfReadPacket = 0;
+ ParsableByteArray payload = oggPacket.getPayload();
+ long granulesInPacket = preparePayload(payload);
+ if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) {
+ // calculate time and send payload data to codec
+ long timeUs = convertGranuleToTime(currentGranule);
+ trackOutput.sampleData(payload, payload.limit());
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null);
+ targetGranule = -1;
+ }
+ currentGranule += granulesInPacket;
+ } else {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ /**
+ * Converts granule value to time.
+ *
+ * @param granule The granule value.
+ * @return Time in milliseconds.
+ */
+ protected long convertGranuleToTime(long granule) {
+ return (granule * C.MICROS_PER_SECOND) / sampleRate;
+ }
+
+ /**
+ * Converts time value to granule.
+ *
+ * @param timeUs Time in milliseconds.
+ * @return The granule value.
+ */
+ protected long convertTimeToGranule(long timeUs) {
+ return (sampleRate * timeUs) / C.MICROS_PER_SECOND;
+ }
+
+ /**
+ * Prepares payload data in the packet for submitting to TrackOutput and returns number of
+ * granules in the packet.
+ *
+ * @param packet Ogg payload data packet.
+ * @return Number of granules in the packet or -1 if the packet doesn't contain payload data.
+ */
+ protected abstract long preparePayload(ParsableByteArray packet);
+
+ /**
+ * Checks if the given packet is a header packet and reads it.
+ *
+ * @param packet An ogg packet.
+ * @param position Position of the given header packet.
+ * @param setupData Setup data to be filled.
+ * @return Whether the packet contains header data.
+ */
+ protected abstract boolean readHeaders(ParsableByteArray packet, long position,
+ SetupData setupData) throws IOException, InterruptedException;
+
+ /**
+ * Called on end of seeking.
+ *
+ * @param currentGranule The granule at the current input position.
+ */
+ protected void onSeekEnd(long currentGranule) {
+ this.currentGranule = currentGranule;
+ }
+
+ private static final class UnseekableOggSeeker implements OggSeeker {
+
+ @Override
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ return -1;
+ }
+
+ @Override
+ public long startSeek(long timeUs) {
+ return 0;
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ return new SeekMap.Unseekable(C.TIME_UNSET);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
+ * specification</a>
+ */
+/* package */ final class VorbisBitArray {
+
+ public final byte[] data;
+ private final int limit;
+ private int byteOffset;
+ private int bitOffset;
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data the array to wrap.
+ */
+ public VorbisBitArray(byte[] data) {
+ this(data, data.length);
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data the array to wrap.
+ * @param limit the limit in bytes.
+ */
+ public VorbisBitArray(byte[] data, int limit) {
+ this.data = data;
+ this.limit = limit * 8;
+ }
+
+ /**
+ * Resets the reading position to zero.
+ */
+ public void reset() {
+ byteOffset = 0;
+ bitOffset = 0;
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return {@code true} if the bit is set, {@code false} otherwise.
+ */
+ public boolean readBit() {
+ return readBits(1) == 1;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom {@code numBits} bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ Assertions.checkState(getPosition() + numBits <= limit);
+ if (numBits == 0) {
+ return 0;
+ }
+ int result = 0;
+ int bitCount = 0;
+ if (bitOffset != 0) {
+ bitCount = Math.min(numBits, 8 - bitOffset);
+ int mask = 0xFF >>> (8 - bitCount);
+ result = (data[byteOffset] >>> bitOffset) & mask;
+ bitOffset += bitCount;
+ if (bitOffset == 8) {
+ byteOffset++;
+ bitOffset = 0;
+ }
+ }
+
+ if (numBits - bitCount > 7) {
+ int numBytes = (numBits - bitCount) / 8;
+ for (int i = 0; i < numBytes; i++) {
+ result |= (data[byteOffset++] & 0xFFL) << bitCount;
+ bitCount += 8;
+ }
+ }
+
+ if (numBits > bitCount) {
+ int bitsOnNextByte = numBits - bitCount;
+ int mask = 0xFF >>> (8 - bitsOnNextByte);
+ result |= (data[byteOffset] & mask) << bitCount;
+ bitOffset += bitsOnNextByte;
+ }
+ return result;
+ }
+
+ /**
+ * Skips {@code numberOfBits} bits.
+ *
+ * @param numberOfBits The number of bits to skip.
+ */
+ public void skipBits(int numberOfBits) {
+ Assertions.checkState(getPosition() + numberOfBits <= limit);
+ byteOffset += numberOfBits / 8;
+ bitOffset += numberOfBits % 8;
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ }
+
+ /**
+ * Returns the reading position in bits.
+ */
+ public int getPosition() {
+ return byteOffset * 8 + bitOffset;
+ }
+
+ /**
+ * Sets the reading position in bits.
+ *
+ * @param position The new reading position in bits.
+ */
+ public void setPosition(int position) {
+ Assertions.checkArgument(position < limit && position >= 0);
+ byteOffset = position / 8;
+ bitOffset = position - (byteOffset * 8);
+ }
+
+ /**
+ * Returns the number of remaining bits.
+ */
+ public int bitsLeft() {
+ return limit - getPosition();
+ }
+
+ /**
+ * Returns the limit in bits.
+ **/
+ public int limit() {
+ return limit;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
@@ -0,0 +1,196 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ogg.VorbisUtil.Mode;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
+ */
+/* package */ final class VorbisReader extends StreamReader {
+
+ private VorbisSetup vorbisSetup;
+ private int previousPacketBlockSize;
+ private boolean seenFirstAudioPacket;
+
+ private VorbisUtil.VorbisIdHeader vorbisIdHeader;
+ private VorbisUtil.CommentHeader commentHeader;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ try {
+ return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ vorbisSetup = null;
+ vorbisIdHeader = null;
+ commentHeader = null;
+ }
+ previousPacketBlockSize = 0;
+ seenFirstAudioPacket = false;
+ }
+
+ @Override
+ protected void onSeekEnd(long currentGranule) {
+ super.onSeekEnd(currentGranule);
+ seenFirstAudioPacket = currentGranule != 0;
+ previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ // if this is not an audio packet...
+ if ((packet.data[0] & 0x01) == 1) {
+ return -1;
+ }
+
+ // ... we need to decode the block size
+ int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup);
+ // a packet contains samples produced from overlapping the previous and current frame data
+ // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
+ int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
+ : 0;
+ // codec expects the number of samples appended to audio data
+ appendNumberOfSamples(packet, samplesInPacket);
+
+ // update state in members for next iteration
+ seenFirstAudioPacket = true;
+ previousPacketBlockSize = packetBlockSize;
+ return samplesInPacket;
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
+ throws IOException, InterruptedException {
+ if (vorbisSetup != null) {
+ return false;
+ }
+
+ vorbisSetup = readSetupHeaders(packet);
+ if (vorbisSetup == null) {
+ return true;
+ }
+
+ ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
+ codecInitialisationData.add(vorbisSetup.idHeader.data);
+ codecInitialisationData.add(vorbisSetup.setupHeaderData);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,
+ this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD,
+ this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
+ codecInitialisationData, null, 0, null);
+ return true;
+ }
+
+ //@VisibleForTesting
+ /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException {
+
+ if (vorbisIdHeader == null) {
+ vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
+ return null;
+ }
+
+ if (commentHeader == null) {
+ commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
+ return null;
+ }
+
+ // the third packet contains the setup header
+ byte[] setupHeaderData = new byte[scratch.limit()];
+ // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
+ System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());
+ // partially decode setup header to get the modes
+ Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
+ // we need the ilog of modes all the time when extracting, so we compute it once
+ int iLogModes = VorbisUtil.iLog(modes.length - 1);
+
+ return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
+ }
+
+ /**
+ * Reads an int of {@code length} bits from {@code src} starting at
+ * {@code leastSignificantBitIndex}.
+ *
+ * @param src the {@code byte} to read from.
+ * @param length the length in bits of the int to read.
+ * @param leastSignificantBitIndex the index of the least significant bit of the int to read.
+ * @return the int value read.
+ */
+ //@VisibleForTesting
+ /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) {
+ return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
+ }
+
+ //@VisibleForTesting
+ /* package */ static void appendNumberOfSamples(ParsableByteArray buffer,
+ long packetSampleCount) {
+
+ buffer.setLimit(buffer.limit() + 4);
+ // The vorbis decoder expects the number of samples in the packet
+ // to be appended to the audio data as an int32
+ buffer.data[buffer.limit() - 4] = (byte) ((packetSampleCount) & 0xFF);
+ buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);
+ buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);
+ buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);
+ }
+
+ private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
+ // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
+ int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
+ int currentBlockSize;
+ if (!vorbisSetup.modes[modeNumber].blockFlag) {
+ currentBlockSize = vorbisSetup.idHeader.blockSize0;
+ } else {
+ currentBlockSize = vorbisSetup.idHeader.blockSize1;
+ }
+ return currentBlockSize;
+ }
+
+ /**
+ * Class to hold all data read from Vorbis setup headers.
+ */
+ /* package */ static final class VorbisSetup {
+
+ public final VorbisUtil.VorbisIdHeader idHeader;
+ public final VorbisUtil.CommentHeader commentHeader;
+ public final byte[] setupHeaderData;
+ public final Mode[] modes;
+ public final int iLogModes;
+
+ public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader
+ commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {
+ this.idHeader = idHeader;
+ this.commentHeader = commentHeader;
+ this.setupHeaderData = setupHeaderData;
+ this.modes = modes;
+ this.iLogModes = iLogModes;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java
@@ -0,0 +1,493 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ogg;
+
+import android.util.Log;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+
+/**
+ * Utility methods for parsing vorbis streams.
+ */
+/* package */ final class VorbisUtil {
+
+ private static final String TAG = "VorbisUtil";
+
+ /**
+ * Returns ilog(x), which is the index of the highest set bit in {@code x}.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1">
+ * Vorbis spec</a>
+ * @param x the value of which the ilog should be calculated.
+ * @return ilog(x)
+ */
+ public static int iLog(int x) {
+ int val = 0;
+ while (x > 0) {
+ val++;
+ x >>>= 1;
+ }
+ return val;
+ }
+
+ /**
+ * Reads a vorbis identification header from {@code headerData}.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
+ * spec/Identification header</a>
+ * @param headerData a {@link ParsableByteArray} wrapping the header data.
+ * @return a {@link VorbisUtil.VorbisIdHeader} with meta data.
+ * @throws ParserException thrown if invalid capture pattern is detected.
+ */
+ public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x01, headerData, false);
+
+ long version = headerData.readLittleEndianUnsignedInt();
+ int channels = headerData.readUnsignedByte();
+ long sampleRate = headerData.readLittleEndianUnsignedInt();
+ int bitrateMax = headerData.readLittleEndianInt();
+ int bitrateNominal = headerData.readLittleEndianInt();
+ int bitrateMin = headerData.readLittleEndianInt();
+
+ int blockSize = headerData.readUnsignedByte();
+ int blockSize0 = (int) Math.pow(2, blockSize & 0x0F);
+ int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4);
+
+ boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0;
+ // raw data of vorbis setup header has to be passed to decoder as CSD buffer #1
+ byte[] data = Arrays.copyOf(headerData.data, headerData.limit());
+
+ return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin,
+ blockSize0, blockSize1, framingFlag, data);
+ }
+
+ /**
+ * Reads a vorbis comment header.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">
+ * Vorbis spec/Comment header</a>
+ * @param headerData a {@link ParsableByteArray} wrapping the header data.
+ * @return a {@link VorbisUtil.CommentHeader} with all the comments.
+ * @throws ParserException thrown if invalid capture pattern is detected.
+ */
+ public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x03, headerData, false);
+ int length = 7;
+
+ int len = (int) headerData.readLittleEndianUnsignedInt();
+ length += 4;
+ String vendor = headerData.readString(len);
+ length += vendor.length();
+
+ long commentListLen = headerData.readLittleEndianUnsignedInt();
+ String[] comments = new String[(int) commentListLen];
+ length += 4;
+ for (int i = 0; i < commentListLen; i++) {
+ len = (int) headerData.readLittleEndianUnsignedInt();
+ length += 4;
+ comments[i] = headerData.readString(len);
+ length += comments[i].length();
+ }
+ if ((headerData.readUnsignedByte() & 0x01) == 0) {
+ throw new ParserException("framing bit expected to be set");
+ }
+ length += 1;
+ return new CommentHeader(vendor, comments, length);
+ }
+
+ /**
+ * Verifies whether the next bytes in {@code header} are a vorbis header of the given
+ * {@code headerType}.
+ *
+ * @param headerType the type of the header expected.
+ * @param header the alleged header bytes.
+ * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned.
+ * @return the number of bytes read.
+ * @throws ParserException thrown if header type or capture pattern is not as expected.
+ */
+ public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header,
+ boolean quiet)
+ throws ParserException {
+ if (header.bytesLeft() < 7) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("too short header: " + header.bytesLeft());
+ }
+ }
+
+ if (header.readUnsignedByte() != headerType) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected header type " + Integer.toHexString(headerType));
+ }
+ }
+
+ if (!(header.readUnsignedByte() == 'v'
+ && header.readUnsignedByte() == 'o'
+ && header.readUnsignedByte() == 'r'
+ && header.readUnsignedByte() == 'b'
+ && header.readUnsignedByte() == 'i'
+ && header.readUnsignedByte() == 's')) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected characters 'vorbis'");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This method reads the modes which are located at the very end of the vorbis setup header.
+ * That's why we need to partially decode or at least read the entire setup header to know
+ * where to start reading the modes.
+ *
+ * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">
+ * Vorbis spec/Setup header</a>
+ * @param headerData a {@link ParsableByteArray} containing setup header data.
+ * @param channels the number of channels.
+ * @return an array of {@link Mode}s.
+ * @throws ParserException thrown if bit stream is invalid.
+ */
+ public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
+ throws ParserException {
+
+ verifyVorbisHeaderCapturePattern(0x05, headerData, false);
+
+ int numberOfBooks = headerData.readUnsignedByte() + 1;
+
+ VorbisBitArray bitArray = new VorbisBitArray(headerData.data);
+ bitArray.skipBits(headerData.getPosition() * 8);
+
+ for (int i = 0; i < numberOfBooks; i++) {
+ readBook(bitArray);
+ }
+
+ int timeCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < timeCount; i++) {
+ if (bitArray.readBits(16) != 0x00) {
+ throw new ParserException("placeholder of time domain transforms not zeroed out");
+ }
+ }
+ readFloors(bitArray);
+ readResidues(bitArray);
+ readMappings(channels, bitArray);
+
+ Mode[] modes = readModes(bitArray);
+ if (!bitArray.readBit()) {
+ throw new ParserException("framing bit after modes not set as expected");
+ }
+ return modes;
+ }
+
+ private static Mode[] readModes(VorbisBitArray bitArray) {
+ int modeCount = bitArray.readBits(6) + 1;
+ Mode[] modes = new Mode[modeCount];
+ for (int i = 0; i < modeCount; i++) {
+ boolean blockFlag = bitArray.readBit();
+ int windowType = bitArray.readBits(16);
+ int transformType = bitArray.readBits(16);
+ int mapping = bitArray.readBits(8);
+ modes[i] = new Mode(blockFlag, windowType, transformType, mapping);
+ }
+ return modes;
+ }
+
+ private static void readMappings(int channels, VorbisBitArray bitArray)
+ throws ParserException {
+ int mappingsCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < mappingsCount; i++) {
+ int mappingType = bitArray.readBits(16);
+ switch (mappingType) {
+ case 0:
+ int submaps;
+ if (bitArray.readBit()) {
+ submaps = bitArray.readBits(4) + 1;
+ } else {
+ submaps = 1;
+ }
+ int couplingSteps;
+ if (bitArray.readBit()) {
+ couplingSteps = bitArray.readBits(8) + 1;
+ for (int j = 0; j < couplingSteps; j++) {
+ bitArray.skipBits(iLog(channels - 1)); // magnitude
+ bitArray.skipBits(iLog(channels - 1)); // angle
+ }
+ } /*else {
+ couplingSteps = 0;
+ }*/
+ if (bitArray.readBits(2) != 0x00) {
+ throw new ParserException("to reserved bits must be zero after mapping coupling steps");
+ }
+ if (submaps > 1) {
+ for (int j = 0; j < channels; j++) {
+ bitArray.skipBits(4); // mappingMux
+ }
+ }
+ for (int j = 0; j < submaps; j++) {
+ bitArray.skipBits(8); // discard
+ bitArray.skipBits(8); // submapFloor
+ bitArray.skipBits(8); // submapResidue
+ }
+ break;
+ default:
+ Log.e(TAG, "mapping type other than 0 not supported: " + mappingType);
+ }
+ }
+ }
+
+ private static void readResidues(VorbisBitArray bitArray) throws ParserException {
+ int residueCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < residueCount; i++) {
+ int residueType = bitArray.readBits(16);
+ if (residueType > 2) {
+ throw new ParserException("residueType greater than 2 is not decodable");
+ } else {
+ bitArray.skipBits(24); // begin
+ bitArray.skipBits(24); // end
+ bitArray.skipBits(24); // partitionSize (add one)
+ int classifications = bitArray.readBits(6) + 1;
+ bitArray.skipBits(8); // classbook
+ int[] cascade = new int[classifications];
+ for (int j = 0; j < classifications; j++) {
+ int highBits = 0;
+ int lowBits = bitArray.readBits(3);
+ if (bitArray.readBit()) {
+ highBits = bitArray.readBits(5);
+ }
+ cascade[j] = highBits * 8 + lowBits;
+ }
+ for (int j = 0; j < classifications; j++) {
+ for (int k = 0; k < 8; k++) {
+ if ((cascade[j] & (0x01 << k)) != 0) {
+ bitArray.skipBits(8); // discard
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void readFloors(VorbisBitArray bitArray) throws ParserException {
+ int floorCount = bitArray.readBits(6) + 1;
+ for (int i = 0; i < floorCount; i++) {
+ int floorType = bitArray.readBits(16);
+ switch (floorType) {
+ case 0:
+ bitArray.skipBits(8); //order
+ bitArray.skipBits(16); // rate
+ bitArray.skipBits(16); // barkMapSize
+ bitArray.skipBits(6); // amplitudeBits
+ bitArray.skipBits(8); // amplitudeOffset
+ int floorNumberOfBooks = bitArray.readBits(4) + 1;
+ for (int j = 0; j < floorNumberOfBooks; j++) {
+ bitArray.skipBits(8);
+ }
+ break;
+ case 1:
+ int partitions = bitArray.readBits(5);
+ int maximumClass = -1;
+ int[] partitionClassList = new int[partitions];
+ for (int j = 0; j < partitions; j++) {
+ partitionClassList[j] = bitArray.readBits(4);
+ if (partitionClassList[j] > maximumClass) {
+ maximumClass = partitionClassList[j];
+ }
+ }
+ int[] classDimensions = new int[maximumClass + 1];
+ for (int j = 0; j < classDimensions.length; j++) {
+ classDimensions[j] = bitArray.readBits(3) + 1;
+ int classSubclasses = bitArray.readBits(2);
+ if (classSubclasses > 0) {
+ bitArray.skipBits(8); // classMasterbooks
+ }
+ for (int k = 0; k < (1 << classSubclasses); k++) {
+ bitArray.skipBits(8); // subclassBook (subtract 1)
+ }
+ }
+ bitArray.skipBits(2); // multiplier (add one)
+ int rangeBits = bitArray.readBits(4);
+ int count = 0;
+ for (int j = 0, k = 0; j < partitions; j++) {
+ int idx = partitionClassList[j];
+ count += classDimensions[idx];
+ for (; k < count; k++) {
+ bitArray.skipBits(rangeBits); // floorValue
+ }
+ }
+ break;
+ default:
+ throw new ParserException("floor type greater than 1 not decodable: " + floorType);
+ }
+ }
+ }
+
+ private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException {
+ if (bitArray.readBits(24) != 0x564342) {
+ throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at "
+ + bitArray.getPosition());
+ }
+ int dimensions = bitArray.readBits(16);
+ int entries = bitArray.readBits(24);
+ long[] lengthMap = new long[entries];
+
+ boolean isOrdered = bitArray.readBit();
+ if (!isOrdered) {
+ boolean isSparse = bitArray.readBit();
+ for (int i = 0; i < lengthMap.length; i++) {
+ if (isSparse) {
+ if (bitArray.readBit()) {
+ lengthMap[i] = bitArray.readBits(5) + 1;
+ } else { // entry unused
+ lengthMap[i] = 0;
+ }
+ } else { // not sparse
+ lengthMap[i] = bitArray.readBits(5) + 1;
+ }
+ }
+ } else {
+ int length = bitArray.readBits(5) + 1;
+ for (int i = 0; i < lengthMap.length;) {
+ int num = bitArray.readBits(iLog(entries - i));
+ for (int j = 0; j < num && i < lengthMap.length; i++, j++) {
+ lengthMap[i] = length;
+ }
+ length++;
+ }
+ }
+
+ int lookupType = bitArray.readBits(4);
+ if (lookupType > 2) {
+ throw new ParserException("lookup type greater than 2 not decodable: " + lookupType);
+ } else if (lookupType == 1 || lookupType == 2) {
+ bitArray.skipBits(32); // minimumValue
+ bitArray.skipBits(32); // deltaValue
+ int valueBits = bitArray.readBits(4) + 1;
+ bitArray.skipBits(1); // sequenceP
+ long lookupValuesCount;
+ if (lookupType == 1) {
+ if (dimensions != 0) {
+ lookupValuesCount = mapType1QuantValues(entries, dimensions);
+ } else {
+ lookupValuesCount = 0;
+ }
+ } else {
+ lookupValuesCount = entries * dimensions;
+ }
+ // discard (no decoding required yet)
+ bitArray.skipBits((int) (lookupValuesCount * valueBits));
+ }
+ return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);
+ }
+
+ /**
+ * @see <a href="http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c">_book_maptype1_quantvals</a>
+ */
+ private static long mapType1QuantValues(long entries, long dimension) {
+ return (long) Math.floor(Math.pow(entries, 1.d / dimension));
+ }
+
+ public static final class CodeBook {
+
+ public final int dimensions;
+ public final int entries;
+ public final long[] lengthMap;
+ public final int lookupType;
+ public final boolean isOrdered;
+
+ public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType,
+ boolean isOrdered) {
+ this.dimensions = dimensions;
+ this.entries = entries;
+ this.lengthMap = lengthMap;
+ this.lookupType = lookupType;
+ this.isOrdered = isOrdered;
+ }
+
+ }
+
+ public static final class CommentHeader {
+
+ public final String vendor;
+ public final String[] comments;
+ public final int length;
+
+ public CommentHeader(String vendor, String[] comments, int length) {
+ this.vendor = vendor;
+ this.comments = comments;
+ this.length = length;
+ }
+
+ }
+
+ public static final class VorbisIdHeader {
+
+ public final long version;
+ public final int channels;
+ public final long sampleRate;
+ public final int bitrateMax;
+ public final int bitrateNominal;
+ public final int bitrateMin;
+ public final int blockSize0;
+ public final int blockSize1;
+ public final boolean framingFlag;
+ public final byte[] data;
+
+ public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
+ int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
+ byte[] data) {
+ this.version = version;
+ this.channels = channels;
+ this.sampleRate = sampleRate;
+ this.bitrateMax = bitrateMax;
+ this.bitrateNominal = bitrateNominal;
+ this.bitrateMin = bitrateMin;
+ this.blockSize0 = blockSize0;
+ this.blockSize1 = blockSize1;
+ this.framingFlag = framingFlag;
+ this.data = data;
+ }
+
+ public int getApproximateBitrate() {
+ return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;
+ }
+
+ }
+
+ public static final class Mode {
+
+ public final boolean blockFlag;
+ public final int windowType;
+ public final int transformType;
+ public final int mapping;
+
+ public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
+ this.blockFlag = blockFlag;
+ this.windowType = windowType;
+ this.transformType = transformType;
+ this.mapping = mapping;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.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 com.google.android.exoplayer2.extractor.rawcc;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Extracts CEA data from a RawCC file.
+ */
+public final class RawCcExtractor implements Extractor {
+
+ private static final int SCRATCH_SIZE = 9;
+ private static final int HEADER_SIZE = 8;
+ private static final int HEADER_ID = Util.getIntegerCodeForString("RCC\u0001");
+ private static final int TIMESTAMP_SIZE_V0 = 4;
+ private static final int TIMESTAMP_SIZE_V1 = 8;
+
+ // Parser states.
+ private static final int STATE_READING_HEADER = 0;
+ private static final int STATE_READING_TIMESTAMP_AND_COUNT = 1;
+ private static final int STATE_READING_SAMPLES = 2;
+
+ private final Format format;
+
+ private final ParsableByteArray dataScratch;
+
+ private TrackOutput trackOutput;
+
+ private int parserState;
+ private int version;
+ private long timestampUs;
+ private int remainingSampleCount;
+ private int sampleBytesWritten;
+
+ public RawCcExtractor(Format format) {
+ this.format = format;
+ dataScratch = new ParsableByteArray(SCRATCH_SIZE);
+ parserState = STATE_READING_HEADER;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
+ output.endTracks();
+ trackOutput.format(format);
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ dataScratch.reset();
+ input.peekFully(dataScratch.data, 0, HEADER_SIZE);
+ return dataScratch.readInt() == HEADER_ID;
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ while (true) {
+ switch (parserState) {
+ case STATE_READING_HEADER:
+ if (parseHeader(input)) {
+ parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ } else {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TIMESTAMP_AND_COUNT:
+ if (parseTimestampAndSampleCount(input)) {
+ parserState = STATE_READING_SAMPLES;
+ } else {
+ parserState = STATE_READING_HEADER;
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_SAMPLES:
+ parseSamples(input);
+ parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ return RESULT_CONTINUE;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ parserState = STATE_READING_HEADER;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
+ dataScratch.reset();
+ if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
+ if (dataScratch.readInt() != HEADER_ID) {
+ throw new IOException("Input not RawCC");
+ }
+ version = dataScratch.readUnsignedByte();
+ // no versions use the flag fields yet
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
+ InterruptedException {
+ dataScratch.reset();
+ if (version == 0) {
+ if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V0 + 1, true)) {
+ return false;
+ }
+ // version 0 timestamps are 45kHz, so we need to convert them into us
+ timestampUs = dataScratch.readUnsignedInt() * 1000 / 45;
+ } else if (version == 1) {
+ if (!input.readFully(dataScratch.data, 0, TIMESTAMP_SIZE_V1 + 1, true)) {
+ return false;
+ }
+ timestampUs = dataScratch.readLong();
+ } else {
+ throw new ParserException("Unsupported version number: " + version);
+ }
+
+ remainingSampleCount = dataScratch.readUnsignedByte();
+ sampleBytesWritten = 0;
+ return true;
+ }
+
+ private void parseSamples(ExtractorInput input) throws IOException, InterruptedException {
+ for (; remainingSampleCount > 0; remainingSampleCount--) {
+ dataScratch.reset();
+ input.readFully(dataScratch.data, 0, 3);
+
+ trackOutput.sampleData(dataScratch, 3);
+ sampleBytesWritten += 3;
+ }
+
+ if (sampleBytesWritten > 0) {
+ trackOutput.sampleMetadata(timestampUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
@@ -0,0 +1,160 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.audio.Ac3Util;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of AC-3 samples from elementary audio files formatted as AC-3
+ * bitstreams.
+ */
+public final class Ac3Extractor implements Extractor {
+
+ /**
+ * Factory for {@link Ac3Extractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new Ac3Extractor()};
+ }
+
+ };
+
+ /**
+ * The maximum number of bytes to search when sniffing, excluding ID3 information, before giving
+ * up.
+ */
+ private static final int MAX_SNIFF_BYTES = 8 * 1024;
+ private static final int AC3_SYNC_WORD = 0x0B77;
+ private static final int MAX_SYNC_FRAME_SIZE = 2786;
+ private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+
+ private final long firstSampleTimestampUs;
+ private final ParsableByteArray sampleData;
+
+ private Ac3Reader reader;
+ private boolean startedPacket;
+
+ public Ac3Extractor() {
+ this(0);
+ }
+
+ public Ac3Extractor(long firstSampleTimestampUs) {
+ this.firstSampleTimestampUs = firstSampleTimestampUs;
+ sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE);
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Skip any ID3 headers.
+ ParsableByteArray scratch = new ParsableByteArray(10);
+ int startPosition = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 10);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != ID3_TAG) {
+ break;
+ }
+ scratch.skipBytes(3);
+ int length = scratch.readSynchSafeInt();
+ startPosition += 10 + length;
+ input.advancePeekPosition(length);
+ }
+ input.resetPeekPosition();
+ input.advancePeekPosition(startPosition);
+
+ int headerPosition = startPosition;
+ int validFramesCount = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 5);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if (syncBytes != AC3_SYNC_WORD) {
+ validFramesCount = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4) {
+ return true;
+ }
+ int frameSize = Ac3Util.parseAc3SyncframeSize(scratch.data);
+ if (frameSize == C.LENGTH_UNSET) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - 5);
+ }
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ reader = new Ac3Reader(); // TODO: Add support for embedded ID3.
+ reader.createTracks(output, new TrackIdGenerator(0, 1));
+ output.endTracks();
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ startedPacket = false;
+ reader.seek();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ int bytesRead = input.read(sampleData.data, 0, MAX_SYNC_FRAME_SIZE);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ // Feed whatever data we have to the reader, regardless of whether the read finished or not.
+ sampleData.setPosition(0);
+ sampleData.setLimit(bytesRead);
+
+ if (!startedPacket) {
+ // Pass data to the reader as though it's contained within a single infinitely long packet.
+ reader.packetStarted(firstSampleTimestampUs, true);
+ startedPacket = true;
+ }
+ // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
+ // unnecessary to copy the data through packetBuffer.
+ reader.consume(sampleData);
+ return RESULT_CONTINUE;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
@@ -0,0 +1,205 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.audio.Ac3Util;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Parses a continuous (E-)AC-3 byte stream and extracts individual samples.
+ */
+public final class Ac3Reader implements ElementaryStreamReader {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_FINDING_SYNC, STATE_READING_HEADER, STATE_READING_SAMPLE})
+ private @interface State {}
+ private static final int STATE_FINDING_SYNC = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ private static final int HEADER_SIZE = 8;
+
+ private final ParsableBitArray headerScratchBits;
+ private final ParsableByteArray headerScratchBytes;
+ private final String language;
+
+ private String trackFormatId;
+ private TrackOutput output;
+
+ @State private int state;
+ private int bytesRead;
+
+ // Used to find the header.
+ private boolean lastByteWas0B;
+
+ // Used when parsing the header.
+ private long sampleDurationUs;
+ private Format format;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ /**
+ * Constructs a new reader for (E-)AC-3 elementary streams.
+ */
+ public Ac3Reader() {
+ this(null);
+ }
+
+ /**
+ * Constructs a new reader for (E-)AC-3 elementary streams.
+ *
+ * @param language Track language.
+ */
+ public Ac3Reader(String language) {
+ headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
+ headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
+ state = STATE_FINDING_SYNC;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ lastByteWas0B = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
+ generator.generateNewId();
+ trackFormatId = generator.getFormatId();
+ output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC:
+ if (skipToNextSync(data)) {
+ state = STATE_READING_HEADER;
+ headerScratchBytes.data[0] = 0x0B;
+ headerScratchBytes.data[1] = 0x77;
+ bytesRead = 2;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, HEADER_SIZE);
+ state = STATE_READING_SAMPLE;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ output.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += sampleDurationUs;
+ state = STATE_FINDING_SYNC;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Locates the next syncword, advancing the position to the byte that immediately follows it. If a
+ * syncword was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ * @return Whether a syncword position was found.
+ */
+ private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+ while (pesBuffer.bytesLeft() > 0) {
+ if (!lastByteWas0B) {
+ lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B;
+ continue;
+ }
+ int secondByte = pesBuffer.readUnsignedByte();
+ if (secondByte == 0x77) {
+ lastByteWas0B = false;
+ return true;
+ } else {
+ lastByteWas0B = secondByte == 0x0B;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void parseHeader() {
+ headerScratchBits.setPosition(0);
+ Ac3Util.Ac3SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits);
+ if (format == null || frameInfo.channelCount != format.channelCount
+ || frameInfo.sampleRate != format.sampleRate
+ || frameInfo.mimeType != format.sampleMimeType) {
+ format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null,
+ Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null,
+ null, 0, language);
+ output.format(format);
+ }
+ sampleSize = frameInfo.frameSize;
+ // In this class a sample is an access unit (syncframe in AC-3), but the MediaFormat sample rate
+ // specifies the number of PCM audio samples per second.
+ sampleDurationUs = C.MICROS_PER_SECOND * frameInfo.sampleCount / format.sampleRate;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -0,0 +1,170 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
+ * headers.
+ */
+public final class AdtsExtractor implements Extractor {
+
+ /**
+ * Factory for {@link AdtsExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new AdtsExtractor()};
+ }
+
+ };
+
+ private static final int MAX_PACKET_SIZE = 200;
+ private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+ /**
+ * The maximum number of bytes to search when sniffing, excluding the header, before giving up.
+ * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes.
+ */
+ private static final int MAX_SNIFF_BYTES = 8 * 1024;
+
+ private final long firstSampleTimestampUs;
+ private final ParsableByteArray packetBuffer;
+
+ // Accessed only by the loading thread.
+ private AdtsReader reader;
+ private boolean startedPacket;
+
+ public AdtsExtractor() {
+ this(0);
+ }
+
+ public AdtsExtractor(long firstSampleTimestampUs) {
+ this.firstSampleTimestampUs = firstSampleTimestampUs;
+ packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Skip any ID3 headers.
+ ParsableByteArray scratch = new ParsableByteArray(10);
+ ParsableBitArray scratchBits = new ParsableBitArray(scratch.data);
+ int startPosition = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 10);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != ID3_TAG) {
+ break;
+ }
+ scratch.skipBytes(3);
+ int length = scratch.readSynchSafeInt();
+ startPosition += 10 + length;
+ input.advancePeekPosition(length);
+ }
+ input.resetPeekPosition();
+ input.advancePeekPosition(startPosition);
+
+ // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size.
+ int headerPosition = startPosition;
+ int validFramesSize = 0;
+ int validFramesCount = 0;
+ while (true) {
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ int syncBytes = scratch.readUnsignedShort();
+ if ((syncBytes & 0xFFF6) != 0xFFF0) {
+ validFramesCount = 0;
+ validFramesSize = 0;
+ input.resetPeekPosition();
+ if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+ return false;
+ }
+ input.advancePeekPosition(headerPosition);
+ } else {
+ if (++validFramesCount >= 4 && validFramesSize > 188) {
+ return true;
+ }
+
+ // Skip the frame.
+ input.peekFully(scratch.data, 0, 4);
+ scratchBits.setPosition(14);
+ int frameSize = scratchBits.readBits(13);
+ // Either the stream is malformed OR we're not parsing an ADTS stream.
+ if (frameSize <= 6) {
+ return false;
+ }
+ input.advancePeekPosition(frameSize - 6);
+ validFramesSize += frameSize;
+ }
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ reader = new AdtsReader(true);
+ reader.createTracks(output, new TrackIdGenerator(0, 1));
+ output.endTracks();
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ startedPacket = false;
+ reader.seek();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ // Feed whatever data we have to the reader, regardless of whether the read finished or not.
+ packetBuffer.setPosition(0);
+ packetBuffer.setLimit(bytesRead);
+
+ if (!startedPacket) {
+ // Pass data to the reader as though it's contained within a single infinitely long packet.
+ reader.packetStarted(firstSampleTimestampUs, true);
+ startedPacket = true;
+ }
+ // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
+ // unnecessary to copy the data through packetBuffer.
+ reader.consume(packetBuffer);
+ return RESULT_CONTINUE;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -0,0 +1,342 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous ADTS byte stream and extracts individual frames.
+ */
+public final class AdtsReader implements ElementaryStreamReader {
+
+ private static final String TAG = "AdtsReader";
+
+ private static final int STATE_FINDING_SAMPLE = 0;
+ private static final int STATE_READING_ID3_HEADER = 1;
+ private static final int STATE_READING_ADTS_HEADER = 2;
+ private static final int STATE_READING_SAMPLE = 3;
+
+ private static final int HEADER_SIZE = 5;
+ private static final int CRC_SIZE = 2;
+
+ // Match states used while looking for the next sample
+ private static final int MATCH_STATE_VALUE_SHIFT = 8;
+ private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT;
+ private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT;
+
+ private static final int ID3_HEADER_SIZE = 10;
+ private static final int ID3_SIZE_OFFSET = 6;
+ private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
+
+ private final boolean exposeId3;
+ private final ParsableBitArray adtsScratch;
+ private final ParsableByteArray id3HeaderBuffer;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+ private TrackOutput id3Output;
+
+ private int state;
+ private int bytesRead;
+
+ private int matchState;
+
+ private boolean hasCrc;
+
+ // Used when parsing the header.
+ private boolean hasOutputFormat;
+ private long sampleDurationUs;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ private TrackOutput currentOutput;
+ private long currentSampleDuration;
+
+ /**
+ * @param exposeId3 True if the reader should expose ID3 information.
+ */
+ public AdtsReader(boolean exposeId3) {
+ this(exposeId3, null);
+ }
+
+ /**
+ * @param exposeId3 True if the reader should expose ID3 information.
+ * @param language Track language.
+ */
+ public AdtsReader(boolean exposeId3, String language) {
+ adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
+ id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
+ setFindingSampleState();
+ this.exposeId3 = exposeId3;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ setFindingSampleState();
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ if (exposeId3) {
+ idGenerator.generateNewId();
+ id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(),
+ MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null));
+ } else {
+ id3Output = new DummyTrackOutput();
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SAMPLE:
+ findNextSample(data);
+ break;
+ case STATE_READING_ID3_HEADER:
+ if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) {
+ parseId3Header();
+ }
+ break;
+ case STATE_READING_ADTS_HEADER:
+ int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
+ if (continueRead(data, adtsScratch.data, targetLength)) {
+ parseAdtsHeader();
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ readSample(data);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Sets the state to STATE_FINDING_SAMPLE.
+ */
+ private void setFindingSampleState() {
+ state = STATE_FINDING_SAMPLE;
+ bytesRead = 0;
+ matchState = MATCH_STATE_START;
+ }
+
+ /**
+ * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for
+ * {@link #parseId3Header()}.
+ */
+ private void setReadingId3HeaderState() {
+ state = STATE_READING_ID3_HEADER;
+ bytesRead = ID3_IDENTIFIER.length;
+ sampleSize = 0;
+ id3HeaderBuffer.setPosition(0);
+ }
+
+ /**
+ * Sets the state to STATE_READING_SAMPLE.
+ *
+ * @param outputToUse TrackOutput object to write the sample to
+ * @param currentSampleDuration Duration of the sample to be read
+ * @param priorReadBytes Size of prior read bytes
+ * @param sampleSize Size of the sample
+ */
+ private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration,
+ int priorReadBytes, int sampleSize) {
+ state = STATE_READING_SAMPLE;
+ bytesRead = priorReadBytes;
+ this.currentOutput = outputToUse;
+ this.currentSampleDuration = currentSampleDuration;
+ this.sampleSize = sampleSize;
+ }
+
+ /**
+ * Sets the state to STATE_READING_ADTS_HEADER.
+ */
+ private void setReadingAdtsHeaderState() {
+ state = STATE_READING_ADTS_HEADER;
+ bytesRead = 0;
+ }
+
+ /**
+ * Locates the next sample start, advancing the position to the byte that immediately follows
+ * identifier. If a sample was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ */
+ private void findNextSample(ParsableByteArray pesBuffer) {
+ byte[] adtsData = pesBuffer.data;
+ int position = pesBuffer.getPosition();
+ int endOffset = pesBuffer.limit();
+ while (position < endOffset) {
+ int data = adtsData[position++] & 0xFF;
+ if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) {
+ hasCrc = (data & 0x1) == 0;
+ setReadingAdtsHeaderState();
+ pesBuffer.setPosition(position);
+ return;
+ }
+ switch (matchState | data) {
+ case MATCH_STATE_START | 0xFF:
+ matchState = MATCH_STATE_FF;
+ break;
+ case MATCH_STATE_START | 'I':
+ matchState = MATCH_STATE_I;
+ break;
+ case MATCH_STATE_I | 'D':
+ matchState = MATCH_STATE_ID;
+ break;
+ case MATCH_STATE_ID | '3':
+ setReadingId3HeaderState();
+ pesBuffer.setPosition(position);
+ return;
+ default:
+ if (matchState != MATCH_STATE_START) {
+ // If matching fails in a later state, revert to MATCH_STATE_START and
+ // check this byte again
+ matchState = MATCH_STATE_START;
+ position--;
+ }
+ break;
+ }
+ }
+ pesBuffer.setPosition(position);
+ }
+
+ /**
+ * Parses the Id3 header.
+ */
+ private void parseId3Header() {
+ id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);
+ id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);
+ setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE,
+ id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ private void parseAdtsHeader() {
+ adtsScratch.setPosition(0);
+
+ if (!hasOutputFormat) {
+ int audioObjectType = adtsScratch.readBits(2) + 1;
+ if (audioObjectType != 2) {
+ // The stream indicates AAC-Main (1), AAC-SSR (3) or AAC-LTP (4). When the stream indicates
+ // AAC-Main it's more likely that the stream contains HE-AAC (5), which cannot be
+ // represented correctly in the 2 bit audio_object_type field in the ADTS header. In
+ // practice when the stream indicates AAC-SSR or AAC-LTP it more commonly contains AAC-LC or
+ // HE-AAC. Since most Android devices don't support AAC-Main, AAC-SSR or AAC-LTP, and since
+ // indicating AAC-LC works for HE-AAC streams, we pretend that we're dealing with AAC-LC and
+ // hope for the best. In practice this often works.
+ // See: https://github.com/google/ExoPlayer/issues/774
+ // See: https://github.com/google/ExoPlayer/issues/1383
+ Log.w(TAG, "Detected audio object type: " + audioObjectType + ", but assuming AAC LC.");
+ audioObjectType = 2;
+ }
+
+ int sampleRateIndex = adtsScratch.readBits(4);
+ adtsScratch.skipBits(1);
+ int channelConfig = adtsScratch.readBits(3);
+
+ byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig(
+ audioObjectType, sampleRateIndex, channelConfig);
+ Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
+ audioSpecificConfig);
+
+ Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
+ Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
+ Collections.singletonList(audioSpecificConfig), null, 0, language);
+ // In this class a sample is an access unit, but the MediaFormat sample rate specifies the
+ // number of PCM audio samples per second.
+ sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
+ output.format(format);
+ hasOutputFormat = true;
+ } else {
+ adtsScratch.skipBits(10);
+ }
+
+ adtsScratch.skipBits(4);
+ int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
+ if (hasCrc) {
+ sampleSize -= CRC_SIZE;
+ }
+
+ setReadingSampleState(output, sampleDurationUs, 0, sampleSize);
+ }
+
+ /**
+ * Reads the rest of the sample
+ */
+ private void readSample(ParsableByteArray data) {
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ currentOutput.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ currentOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += currentSampleDuration;
+ setFindingSampleState();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -0,0 +1,180 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.support.annotation.IntDef;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Default implementation for {@link TsPayloadReader.Factory}.
+ */
+public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory {
+
+ /**
+ * Flags controlling elementary stream readers' behavior.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM,
+ FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM,
+ FLAG_OVERRIDE_CAPTION_DESCRIPTORS})
+ public @interface Flags {}
+ public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
+ public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1;
+ public static final int FLAG_IGNORE_H264_STREAM = 1 << 2;
+ public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3;
+ public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4;
+ public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
+
+ private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
+
+ @Flags private final int flags;
+ private final List<Format> closedCaptionFormats;
+
+ public DefaultTsPayloadReaderFactory() {
+ this(0);
+ }
+
+ /**
+ * @param flags A combination of {@code FLAG_*} values that control the behavior of the created
+ * readers.
+ */
+ public DefaultTsPayloadReaderFactory(@Flags int flags) {
+ this(flags, Collections.<Format>emptyList());
+ }
+
+ /**
+ * @param flags A combination of {@code FLAG_*} values that control the behavior of the created
+ * readers.
+ * @param closedCaptionFormats {@link Format}s to be exposed by payload readers for streams with
+ * embedded closed captions when no caption service descriptors are provided. If
+ * {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, {@code closedCaptionFormats} overrides
+ * any descriptor information. If not set, and {@code closedCaptionFormats} is empty, a
+ * closed caption track with {@link Format#accessibilityChannel} {@link Format#NO_VALUE} will
+ * be exposed.
+ */
+ public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) {
+ this.flags = flags;
+ if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) {
+ closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null,
+ MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null));
+ }
+ this.closedCaptionFormats = closedCaptionFormats;
+ }
+
+ @Override
+ public SparseArray<TsPayloadReader> createInitialPayloadReaders() {
+ return new SparseArray<>();
+ }
+
+ @Override
+ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
+ switch (streamType) {
+ case TsExtractor.TS_STREAM_TYPE_MPA:
+ case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
+ return new PesReader(new MpegAudioReader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AAC:
+ return isSet(FLAG_IGNORE_AAC_STREAM)
+ ? null : new PesReader(new AdtsReader(false, esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_AC3:
+ case TsExtractor.TS_STREAM_TYPE_E_AC3:
+ return new PesReader(new Ac3Reader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_DTS:
+ case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
+ return new PesReader(new DtsReader(esInfo.language));
+ case TsExtractor.TS_STREAM_TYPE_H262:
+ return new PesReader(new H262Reader());
+ case TsExtractor.TS_STREAM_TYPE_H264:
+ return isSet(FLAG_IGNORE_H264_STREAM) ? null
+ : new PesReader(new H264Reader(buildSeiReader(esInfo),
+ isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS)));
+ case TsExtractor.TS_STREAM_TYPE_H265:
+ return new PesReader(new H265Reader(buildSeiReader(esInfo)));
+ case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
+ return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
+ ? null : new SectionReader(new SpliceInfoSectionReader());
+ case TsExtractor.TS_STREAM_TYPE_ID3:
+ return new PesReader(new Id3Reader());
+ case TsExtractor.TS_STREAM_TYPE_DVBSUBS:
+ return new PesReader(
+ new DvbSubtitleReader(esInfo.dvbSubtitleInfos));
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for
+ * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
+ * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor
+ * is not present.
+ *
+ * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
+ * @return A {@link SeiReader} for closed caption tracks.
+ */
+ private SeiReader buildSeiReader(EsInfo esInfo) {
+ if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) {
+ return new SeiReader(closedCaptionFormats);
+ }
+ ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes);
+ List<Format> closedCaptionFormats = this.closedCaptionFormats;
+ while (scratchDescriptorData.bytesLeft() > 0) {
+ int descriptorTag = scratchDescriptorData.readUnsignedByte();
+ int descriptorLength = scratchDescriptorData.readUnsignedByte();
+ int nextDescriptorPosition = scratchDescriptorData.getPosition() + descriptorLength;
+ if (descriptorTag == DESCRIPTOR_TAG_CAPTION_SERVICE) {
+ // Note: see ATSC A/65 for detailed information about the caption service descriptor.
+ closedCaptionFormats = new ArrayList<>();
+ int numberOfServices = scratchDescriptorData.readUnsignedByte() & 0x1F;
+ for (int i = 0; i < numberOfServices; i++) {
+ String language = scratchDescriptorData.readString(3);
+ int captionTypeByte = scratchDescriptorData.readUnsignedByte();
+ boolean isDigital = (captionTypeByte & 0x80) != 0;
+ String mimeType;
+ int accessibilityChannel;
+ if (isDigital) {
+ mimeType = MimeTypes.APPLICATION_CEA708;
+ accessibilityChannel = captionTypeByte & 0x3F;
+ } else {
+ mimeType = MimeTypes.APPLICATION_CEA608;
+ accessibilityChannel = 1;
+ }
+ closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null,
+ Format.NO_VALUE, 0, language, accessibilityChannel, null));
+ // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14).
+ scratchDescriptorData.skipBytes(2);
+ }
+ } else {
+ // Unknown descriptor. Ignore.
+ }
+ scratchDescriptorData.setPosition(nextDescriptorPosition);
+ }
+ return new SeiReader(closedCaptionFormats);
+ }
+
+ private boolean isSet(@Flags int flag) {
+ return (flags & flag) != 0;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
@@ -0,0 +1,181 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.audio.DtsUtil;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous DTS byte stream and extracts individual samples.
+ */
+public final class DtsReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_SYNC = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_SAMPLE = 2;
+
+ private static final int HEADER_SIZE = 15;
+ private static final int SYNC_VALUE = 0x7FFE8001;
+ private static final int SYNC_VALUE_SIZE = 4;
+
+ private final ParsableByteArray headerScratchBytes;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+
+ private int state;
+ private int bytesRead;
+
+ // Used to find the header.
+ private int syncBytes;
+
+ // Used when parsing the header.
+ private long sampleDurationUs;
+ private Format format;
+ private int sampleSize;
+
+ // Used when reading the samples.
+ private long timeUs;
+
+ /**
+ * Constructs a new reader for DTS elementary streams.
+ *
+ * @param language Track language.
+ */
+ public DtsReader(String language) {
+ headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
+ headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
+ headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
+ headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF);
+ headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF);
+ state = STATE_FINDING_SYNC;
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_SYNC;
+ bytesRead = 0;
+ syncBytes = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_SYNC:
+ if (skipToNextSync(data)) {
+ bytesRead = SYNC_VALUE_SIZE;
+ state = STATE_READING_HEADER;
+ }
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) {
+ parseHeader();
+ headerScratchBytes.setPosition(0);
+ output.sampleData(headerScratchBytes, HEADER_SIZE);
+ state = STATE_READING_SAMPLE;
+ }
+ break;
+ case STATE_READING_SAMPLE:
+ int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+ output.sampleData(data, bytesToRead);
+ bytesRead += bytesToRead;
+ if (bytesRead == sampleSize) {
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ timeUs += sampleDurationUs;
+ state = STATE_FINDING_SYNC;
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length was reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ source.readBytes(target, bytesRead, bytesToRead);
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ /**
+ * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately
+ * follows it. If SYNC was not located, the position is advanced to the limit.
+ *
+ * @param pesBuffer The buffer whose position should be advanced.
+ * @return Whether SYNC was found.
+ */
+ private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+ while (pesBuffer.bytesLeft() > 0) {
+ syncBytes <<= 8;
+ syncBytes |= pesBuffer.readUnsignedByte();
+ if (syncBytes == SYNC_VALUE) {
+ syncBytes = 0;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses the sample header.
+ */
+ private void parseHeader() {
+ byte[] frameData = headerScratchBytes.data;
+ if (format == null) {
+ format = DtsUtil.parseDtsFormat(frameData, formatId, language, null);
+ output.format(format);
+ }
+ sampleSize = DtsUtil.getDtsFrameSize(frameData);
+ // In this class a sample is an access unit (frame in DTS), but the format's sample rate
+ // specifies the number of PCM audio samples per second.
+ sampleDurationUs = (int) (C.MICROS_PER_SECOND
+ * DtsUtil.parseDtsAudioSampleCount(frameData) / format.sampleRate);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
@@ -0,0 +1,121 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses DVB subtitle data and extracts individual frames.
+ */
+public final class DvbSubtitleReader implements ElementaryStreamReader {
+
+ private final List<DvbSubtitleInfo> subtitleInfos;
+ private final TrackOutput[] outputs;
+
+ private boolean writingSample;
+ private int bytesToCheck;
+ private int sampleBytesWritten;
+ private long sampleTimeUs;
+
+ /**
+ * @param subtitleInfos Information about the DVB subtitles associated to the stream.
+ */
+ public DvbSubtitleReader(List<DvbSubtitleInfo> subtitleInfos) {
+ this.subtitleInfos = subtitleInfos;
+ outputs = new TrackOutput[subtitleInfos.size()];
+ }
+
+ @Override
+ public void seek() {
+ writingSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i);
+ idGenerator.generateNewId();
+ TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
+ output.format(Format.createImageSampleFormat(idGenerator.getFormatId(),
+ MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE,
+ Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, null));
+ outputs[i] = output;
+ }
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ if (!dataAlignmentIndicator) {
+ return;
+ }
+ writingSample = true;
+ sampleTimeUs = pesTimeUs;
+ sampleBytesWritten = 0;
+ bytesToCheck = 2;
+ }
+
+ @Override
+ public void packetFinished() {
+ if (writingSample) {
+ for (TrackOutput output : outputs) {
+ output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytesWritten, 0, null);
+ }
+ writingSample = false;
+ }
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ if (writingSample) {
+ if (bytesToCheck == 2 && !checkNextByte(data, 0x20)) {
+ // Failed to check data_identifier
+ return;
+ }
+ if (bytesToCheck == 1 && !checkNextByte(data, 0x00)) {
+ // Check and discard the subtitle_stream_id
+ return;
+ }
+ int dataPosition = data.getPosition();
+ int bytesAvailable = data.bytesLeft();
+ for (TrackOutput output : outputs) {
+ data.setPosition(dataPosition);
+ output.sampleData(data, bytesAvailable);
+ }
+ sampleBytesWritten += bytesAvailable;
+ }
+ }
+
+ private boolean checkNextByte(ParsableByteArray data, int expectedValue) {
+ if (data.bytesLeft() == 0) {
+ return false;
+ }
+ if (data.readUnsignedByte() != expectedValue) {
+ writingSample = false;
+ }
+ bytesToCheck--;
+ return writingSample;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Extracts individual samples from an elementary media stream, preserving original order.
+ */
+public interface ElementaryStreamReader {
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ */
+ void seek();
+
+ /**
+ * Initializes the reader by providing outputs and ids for the tracks.
+ *
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void createTracks(ExtractorOutput extractorOutput, PesReader.TrackIdGenerator idGenerator);
+
+ /**
+ * Called when a packet starts.
+ *
+ * @param pesTimeUs The timestamp associated with the packet.
+ * @param dataAlignmentIndicator The data alignment indicator associated with the packet.
+ */
+ void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator);
+
+ /**
+ * Consumes (possibly partial) data from the current packet.
+ *
+ * @param data The data to consume.
+ */
+ void consume(ParsableByteArray data);
+
+ /**
+ * Called when a packet ends.
+ */
+ void packetFinished();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -0,0 +1,289 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H262 byte stream and extracts individual frames.
+ */
+public final class H262Reader implements ElementaryStreamReader {
+
+ private static final int START_PICTURE = 0x00;
+ private static final int START_SEQUENCE_HEADER = 0xB3;
+ private static final int START_EXTENSION = 0xB5;
+ private static final int START_GROUP = 0xB8;
+
+ private String formatId;
+ private TrackOutput output;
+
+ // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
+ private static final double[] FRAME_RATE_VALUES = new double[] {
+ 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+ private long frameDurationUs;
+
+ // State that should be reset on seek.
+ private final boolean[] prefixFlags;
+ private final CsdBuffer csdBuffer;
+ private boolean foundFirstFrameInGroup;
+ private long totalBytesWritten;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+ private boolean pesPtsUsAvailable;
+
+ // Per sample state that gets reset at the start of each frame.
+ private boolean isKeyframe;
+ private long framePosition;
+ private long frameTimeUs;
+
+ public H262Reader() {
+ prefixFlags = new boolean[4];
+ csdBuffer = new CsdBuffer(128);
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ csdBuffer.reset();
+ pesPtsUsAvailable = false;
+ foundFirstFrameInGroup = false;
+ totalBytesWritten = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
+ if (pesPtsUsAvailable) {
+ this.pesTimeUs = pesTimeUs;
+ }
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ int searchOffset = offset;
+ while (true) {
+ int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags);
+
+ if (startCodeOffset == limit) {
+ // We've scanned to the end of the data without finding another start code.
+ if (!hasOutputFormat) {
+ csdBuffer.onData(dataArray, offset, limit);
+ }
+ return;
+ }
+
+ // We've found a start code with the following value.
+ int startCodeValue = data.data[startCodeOffset + 3] & 0xFF;
+
+ if (!hasOutputFormat) {
+ // This is the number of bytes from the current offset to the start of the next start
+ // code. It may be negative if the start code started in the previously consumed data.
+ int lengthToStartCode = startCodeOffset - offset;
+ if (lengthToStartCode > 0) {
+ csdBuffer.onData(dataArray, offset, startCodeOffset);
+ }
+ // This is the number of bytes belonging to the next start code that have already been
+ // passed to csdDataTargetBuffer.
+ int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0;
+ if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) {
+ // The csd data is complete, so we can decode and output the media format.
+ Pair<Format, Long> result = parseCsdBuffer(csdBuffer, formatId);
+ output.format(result.first);
+ frameDurationUs = result.second;
+ hasOutputFormat = true;
+ }
+ }
+
+ if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) {
+ int bytesWrittenPastStartCode = limit - startCodeOffset;
+ if (foundFirstFrameInGroup) {
+ @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode;
+ output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null);
+ isKeyframe = false;
+ }
+ if (startCodeValue == START_GROUP) {
+ foundFirstFrameInGroup = false;
+ isKeyframe = true;
+ } else /* startCodeValue == START_PICTURE */ {
+ frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs);
+ framePosition = totalBytesWritten - bytesWrittenPastStartCode;
+ pesPtsUsAvailable = false;
+ foundFirstFrameInGroup = true;
+ }
+ }
+
+ offset = startCodeOffset;
+ searchOffset = offset + 3;
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Parses the {@link Format} and frame duration from a csd buffer.
+ *
+ * @param csdBuffer The csd buffer.
+ * @param formatId The id for the generated format. May be null.
+ * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or
+ * 0 if the duration could not be determined.
+ */
+ private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
+ byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
+
+ int firstByte = csdData[4] & 0xFF;
+ int secondByte = csdData[5] & 0xFF;
+ int thirdByte = csdData[6] & 0xFF;
+ int width = (firstByte << 4) | (secondByte >> 4);
+ int height = (secondByte & 0x0F) << 8 | thirdByte;
+
+ float pixelWidthHeightRatio = 1f;
+ int aspectRatioCode = (csdData[7] & 0xF0) >> 4;
+ switch(aspectRatioCode) {
+ case 2:
+ pixelWidthHeightRatio = (4 * height) / (float) (3 * width);
+ break;
+ case 3:
+ pixelWidthHeightRatio = (16 * height) / (float) (9 * width);
+ break;
+ case 4:
+ pixelWidthHeightRatio = (121 * height) / (float) (100 * width);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+
+ Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null,
+ Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE,
+ Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null);
+
+ long frameDurationUs = 0;
+ int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1;
+ if (0 <= frameRateCodeMinusOne && frameRateCodeMinusOne < FRAME_RATE_VALUES.length) {
+ double frameRate = FRAME_RATE_VALUES[frameRateCodeMinusOne];
+ int sequenceExtensionPosition = csdBuffer.sequenceExtensionPosition;
+ int frameRateExtensionN = (csdData[sequenceExtensionPosition + 9] & 0x60) >> 5;
+ int frameRateExtensionD = (csdData[sequenceExtensionPosition + 9] & 0x1F);
+ if (frameRateExtensionN != frameRateExtensionD) {
+ frameRate *= (frameRateExtensionN + 1d) / (frameRateExtensionD + 1);
+ }
+ frameDurationUs = (long) (C.MICROS_PER_SECOND / frameRate);
+ }
+
+ return Pair.create(format, frameDurationUs);
+ }
+
+ private static final class CsdBuffer {
+
+ private boolean isFilling;
+
+ public int length;
+ public int sequenceExtensionPosition;
+ public byte[] data;
+
+ public CsdBuffer(int initialCapacity) {
+ data = new byte[initialCapacity];
+ }
+
+ /**
+ * Resets the buffer, clearing any data that it holds.
+ */
+ public void reset() {
+ isFilling = false;
+ length = 0;
+ sequenceExtensionPosition = 0;
+ }
+
+ /**
+ * Called when a start code is encountered in the stream.
+ *
+ * @param startCodeValue The start code value.
+ * @param bytesAlreadyPassed The number of bytes of the start code that have already been
+ * passed to {@link #onData(byte[], int, int)}, or 0.
+ * @return Whether the csd data is now complete. If true is returned, neither
+ * this method or {@link #onData(byte[], int, int)} should be called again without an
+ * interleaving call to {@link #reset()}.
+ */
+ public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) {
+ if (isFilling) {
+ if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) {
+ sequenceExtensionPosition = length;
+ } else {
+ length -= bytesAlreadyPassed;
+ isFilling = false;
+ return true;
+ }
+ } else if (startCodeValue == START_SEQUENCE_HEADER) {
+ isFilling = true;
+ }
+ return false;
+ }
+
+ /**
+ * Called to pass stream data.
+ *
+ * @param newData Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void onData(byte[] newData, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (data.length < length + readLength) {
+ data = Arrays.copyOf(data, (length + readLength) * 2);
+ }
+ System.arraycopy(newData, offset, data, length, readLength);
+ length += readLength;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
@@ -0,0 +1,521 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parses a continuous H264 byte stream and extracts individual frames.
+ */
+public final class H264Reader implements ElementaryStreamReader {
+
+ private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+ private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
+ private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
+
+ private final SeiReader seiReader;
+ private final boolean allowNonIdrKeyframes;
+ private final boolean detectAccessUnits;
+ private final NalUnitTargetBuffer sps;
+ private final NalUnitTargetBuffer pps;
+ private final NalUnitTargetBuffer sei;
+ private long totalBytesWritten;
+ private final boolean[] prefixFlags;
+
+ private String formatId;
+ private TrackOutput output;
+ private SampleReader sampleReader;
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+
+ // Scratch variables to avoid allocations.
+ private final ParsableByteArray seiWrapper;
+
+ /**
+ * @param seiReader An SEI reader for consuming closed caption channels.
+ * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
+ * synchronization samples (key-frames).
+ * @param detectAccessUnits Whether to split the input stream into access units (samples) based on
+ * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
+ */
+ public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
+ this.seiReader = seiReader;
+ this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+ this.detectAccessUnits = detectAccessUnits;
+ prefixFlags = new boolean[3];
+ sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
+ pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
+ sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
+ seiWrapper = new ParsableByteArray();
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ sps.reset();
+ pps.reset();
+ sei.reset();
+ sampleReader.reset();
+ totalBytesWritten = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
+ seiReader.createTracks(extractorOutput, idGenerator);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ this.pesTimeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ // Scan the appended data, processing NAL units as they are encountered
+ while (true) {
+ int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (nalUnitOffset == limit) {
+ // We've scanned to the end of the data without finding the start of another NAL unit.
+ nalUnitData(dataArray, offset, limit);
+ return;
+ }
+
+ // We've seen the start of a NAL unit of the following type.
+ int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset);
+
+ // This is the number of bytes from the current offset to the start of the next NAL unit.
+ // It may be negative if the NAL unit started in the previously consumed data.
+ int lengthToNalUnit = nalUnitOffset - offset;
+ if (lengthToNalUnit > 0) {
+ nalUnitData(dataArray, offset, nalUnitOffset);
+ }
+ int bytesWrittenPastPosition = limit - nalUnitOffset;
+ long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
+ // Indicate the end of the previous NAL unit. If the length to the start of the next unit
+ // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
+ // when notifying that the unit has ended.
+ endNalUnit(absolutePosition, bytesWrittenPastPosition,
+ lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);
+ // Indicate the start of the next NAL unit.
+ startNalUnit(absolutePosition, nalUnitType, pesTimeUs);
+ // Continue scanning the data.
+ offset = nalUnitOffset + 3;
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ private void startNalUnit(long position, int nalUnitType, long pesTimeUs) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.startNalUnit(nalUnitType);
+ pps.startNalUnit(nalUnitType);
+ }
+ sei.startNalUnit(nalUnitType);
+ sampleReader.startNalUnit(position, nalUnitType, pesTimeUs);
+ }
+
+ private void nalUnitData(byte[] dataArray, int offset, int limit) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.appendToNalUnit(dataArray, offset, limit);
+ pps.appendToNalUnit(dataArray, offset, limit);
+ }
+ sei.appendToNalUnit(dataArray, offset, limit);
+ sampleReader.appendToNalUnit(dataArray, offset, limit);
+ }
+
+ private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
+ if (!hasOutputFormat || sampleReader.needsSpsPps()) {
+ sps.endNalUnit(discardPadding);
+ pps.endNalUnit(discardPadding);
+ if (!hasOutputFormat) {
+ if (sps.isCompleted() && pps.isCompleted()) {
+ List<byte[]> initializationData = new ArrayList<>();
+ initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength));
+ initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength));
+ NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
+ NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
+ output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null,
+ Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE,
+ initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null));
+ hasOutputFormat = true;
+ sampleReader.putSps(spsData);
+ sampleReader.putPps(ppsData);
+ sps.reset();
+ pps.reset();
+ }
+ } else if (sps.isCompleted()) {
+ NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
+ sampleReader.putSps(spsData);
+ sps.reset();
+ } else if (pps.isCompleted()) {
+ NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
+ sampleReader.putPps(ppsData);
+ pps.reset();
+ }
+ }
+ if (sei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);
+ seiWrapper.reset(sei.nalData, unescapedLength);
+ seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ sampleReader.endNalUnit(position, offset);
+ }
+
+ /**
+ * Consumes a stream of NAL units and outputs samples.
+ */
+ private static final class SampleReader {
+
+ private static final int DEFAULT_BUFFER_SIZE = 128;
+
+ private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture
+ private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A
+ private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture
+ private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter
+
+ private final TrackOutput output;
+ private final boolean allowNonIdrKeyframes;
+ private final boolean detectAccessUnits;
+ private final SparseArray<NalUnitUtil.SpsData> sps;
+ private final SparseArray<NalUnitUtil.PpsData> pps;
+ private final ParsableNalUnitBitArray bitArray;
+
+ private byte[] buffer;
+ private int bufferLength;
+
+ // Per NAL unit state. A sample consists of one or more NAL units.
+ private int nalUnitType;
+ private long nalUnitStartPosition;
+ private boolean isFilling;
+ private long nalUnitTimeUs;
+ private SliceHeaderData previousSliceHeader;
+ private SliceHeaderData sliceHeader;
+
+ // Per sample state that gets reset at the start of each sample.
+ private boolean readingSample;
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+
+ public SampleReader(TrackOutput output, boolean allowNonIdrKeyframes,
+ boolean detectAccessUnits) {
+ this.output = output;
+ this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+ this.detectAccessUnits = detectAccessUnits;
+ sps = new SparseArray<>();
+ pps = new SparseArray<>();
+ previousSliceHeader = new SliceHeaderData();
+ sliceHeader = new SliceHeaderData();
+ buffer = new byte[DEFAULT_BUFFER_SIZE];
+ bitArray = new ParsableNalUnitBitArray(buffer, 0, 0);
+ reset();
+ }
+
+ public boolean needsSpsPps() {
+ return detectAccessUnits;
+ }
+
+ public void putSps(NalUnitUtil.SpsData spsData) {
+ sps.append(spsData.seqParameterSetId, spsData);
+ }
+
+ public void putPps(NalUnitUtil.PpsData ppsData) {
+ pps.append(ppsData.picParameterSetId, ppsData);
+ }
+
+ public void reset() {
+ isFilling = false;
+ readingSample = false;
+ sliceHeader.clear();
+ }
+
+ public void startNalUnit(long position, int type, long pesTimeUs) {
+ nalUnitType = type;
+ nalUnitTimeUs = pesTimeUs;
+ nalUnitStartPosition = position;
+ if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR)
+ || (detectAccessUnits && (nalUnitType == NAL_UNIT_TYPE_IDR
+ || nalUnitType == NAL_UNIT_TYPE_NON_IDR
+ || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) {
+ // Store the previous header and prepare to populate the new one.
+ SliceHeaderData newSliceHeader = previousSliceHeader;
+ previousSliceHeader = sliceHeader;
+ sliceHeader = newSliceHeader;
+ sliceHeader.clear();
+ bufferLength = 0;
+ isFilling = true;
+ }
+ }
+
+ /**
+ * Called to pass stream data. The data passed should not include the 3 byte start code.
+ *
+ * @param data Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void appendToNalUnit(byte[] data, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (buffer.length < bufferLength + readLength) {
+ buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2);
+ }
+ System.arraycopy(data, offset, buffer, bufferLength, readLength);
+ bufferLength += readLength;
+
+ bitArray.reset(buffer, 0, bufferLength);
+ if (!bitArray.canReadBits(8)) {
+ return;
+ }
+ bitArray.skipBits(1); // forbidden_zero_bit
+ int nalRefIdc = bitArray.readBits(2);
+ bitArray.skipBits(5); // nal_unit_type
+
+ // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013)
+ // subsection 7.3.3.
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // first_mb_in_slice
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ int sliceType = bitArray.readUnsignedExpGolombCodedInt();
+ if (!detectAccessUnits) {
+ // There are AUDs in the stream so the rest of the header can be ignored.
+ isFilling = false;
+ sliceHeader.setSliceType(sliceType);
+ return;
+ }
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ int picParameterSetId = bitArray.readUnsignedExpGolombCodedInt();
+ if (pps.indexOfKey(picParameterSetId) < 0) {
+ // We have not seen the PPS yet, so don't try to decode the slice header.
+ isFilling = false;
+ return;
+ }
+ NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId);
+ NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId);
+ if (spsData.separateColorPlaneFlag) {
+ if (!bitArray.canReadBits(2)) {
+ return;
+ }
+ bitArray.skipBits(2); // colour_plane_id
+ }
+ if (!bitArray.canReadBits(spsData.frameNumLength)) {
+ return;
+ }
+ boolean fieldPicFlag = false;
+ boolean bottomFieldFlagPresent = false;
+ boolean bottomFieldFlag = false;
+ int frameNum = bitArray.readBits(spsData.frameNumLength);
+ if (!spsData.frameMbsOnlyFlag) {
+ if (!bitArray.canReadBits(1)) {
+ return;
+ }
+ fieldPicFlag = bitArray.readBit();
+ if (fieldPicFlag) {
+ if (!bitArray.canReadBits(1)) {
+ return;
+ }
+ bottomFieldFlag = bitArray.readBit();
+ bottomFieldFlagPresent = true;
+ }
+ }
+ boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR;
+ int idrPicId = 0;
+ if (idrPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ idrPicId = bitArray.readUnsignedExpGolombCodedInt();
+ }
+ int picOrderCntLsb = 0;
+ int deltaPicOrderCntBottom = 0;
+ int deltaPicOrderCnt0 = 0;
+ int deltaPicOrderCnt1 = 0;
+ if (spsData.picOrderCountType == 0) {
+ if (!bitArray.canReadBits(spsData.picOrderCntLsbLength)) {
+ return;
+ }
+ picOrderCntLsb = bitArray.readBits(spsData.picOrderCntLsbLength);
+ if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCntBottom = bitArray.readSignedExpGolombCodedInt();
+ }
+ } else if (spsData.picOrderCountType == 1
+ && !spsData.deltaPicOrderAlwaysZeroFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCnt0 = bitArray.readSignedExpGolombCodedInt();
+ if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) {
+ if (!bitArray.canReadExpGolombCodedNum()) {
+ return;
+ }
+ deltaPicOrderCnt1 = bitArray.readSignedExpGolombCodedInt();
+ }
+ }
+ sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag,
+ bottomFieldFlagPresent, bottomFieldFlag, idrPicFlag, idrPicId, picOrderCntLsb,
+ deltaPicOrderCntBottom, deltaPicOrderCnt0, deltaPicOrderCnt1);
+ isFilling = false;
+ }
+
+ public void endNalUnit(long position, int offset) {
+ if (nalUnitType == NAL_UNIT_TYPE_AUD
+ || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
+ // If the NAL unit ending is the start of a new sample, output the previous one.
+ if (readingSample) {
+ int nalUnitLength = (int) (position - nalUnitStartPosition);
+ outputSample(offset + nalUnitLength);
+ }
+ samplePosition = nalUnitStartPosition;
+ sampleTimeUs = nalUnitTimeUs;
+ sampleIsKeyframe = false;
+ readingSample = true;
+ }
+ sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes
+ && nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice());
+ }
+
+ private void outputSample(int offset) {
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (nalUnitStartPosition - samplePosition);
+ output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
+ }
+
+ private static final class SliceHeaderData {
+
+ private static final int SLICE_TYPE_I = 2;
+ private static final int SLICE_TYPE_ALL_I = 7;
+
+ private boolean isComplete;
+ private boolean hasSliceType;
+
+ private SpsData spsData;
+ private int nalRefIdc;
+ private int sliceType;
+ private int frameNum;
+ private int picParameterSetId;
+ private boolean fieldPicFlag;
+ private boolean bottomFieldFlagPresent;
+ private boolean bottomFieldFlag;
+ private boolean idrPicFlag;
+ private int idrPicId;
+ private int picOrderCntLsb;
+ private int deltaPicOrderCntBottom;
+ private int deltaPicOrderCnt0;
+ private int deltaPicOrderCnt1;
+
+ public void clear() {
+ hasSliceType = false;
+ isComplete = false;
+ }
+
+ public void setSliceType(int sliceType) {
+ this.sliceType = sliceType;
+ hasSliceType = true;
+ }
+
+ public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum,
+ int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent,
+ boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb,
+ int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) {
+ this.spsData = spsData;
+ this.nalRefIdc = nalRefIdc;
+ this.sliceType = sliceType;
+ this.frameNum = frameNum;
+ this.picParameterSetId = picParameterSetId;
+ this.fieldPicFlag = fieldPicFlag;
+ this.bottomFieldFlagPresent = bottomFieldFlagPresent;
+ this.bottomFieldFlag = bottomFieldFlag;
+ this.idrPicFlag = idrPicFlag;
+ this.idrPicId = idrPicId;
+ this.picOrderCntLsb = picOrderCntLsb;
+ this.deltaPicOrderCntBottom = deltaPicOrderCntBottom;
+ this.deltaPicOrderCnt0 = deltaPicOrderCnt0;
+ this.deltaPicOrderCnt1 = deltaPicOrderCnt1;
+ isComplete = true;
+ hasSliceType = true;
+ }
+
+ public boolean isISlice() {
+ return hasSliceType && (sliceType == SLICE_TYPE_ALL_I || sliceType == SLICE_TYPE_I);
+ }
+
+ private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
+ // See ISO 14496-10 subsection 7.4.1.2.4.
+ return isComplete && (!other.isComplete || frameNum != other.frameNum
+ || picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag
+ || (bottomFieldFlagPresent && other.bottomFieldFlagPresent
+ && bottomFieldFlag != other.bottomFieldFlag)
+ || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
+ || (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0
+ && (picOrderCntLsb != other.picOrderCntLsb
+ || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
+ || (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1
+ && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
+ || deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
+ || idrPicFlag != other.idrPicFlag
+ || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
+ }
+
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -0,0 +1,493 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H.265 byte stream and extracts individual frames.
+ */
+public final class H265Reader implements ElementaryStreamReader {
+
+ private static final String TAG = "H265Reader";
+
+ // nal_unit_type values from H.265/HEVC (2014) Table 7-1.
+ private static final int RASL_R = 9;
+ private static final int BLA_W_LP = 16;
+ private static final int CRA_NUT = 21;
+ private static final int VPS_NUT = 32;
+ private static final int SPS_NUT = 33;
+ private static final int PPS_NUT = 34;
+ private static final int PREFIX_SEI_NUT = 39;
+ private static final int SUFFIX_SEI_NUT = 40;
+
+ private final SeiReader seiReader;
+
+ private String formatId;
+ private TrackOutput output;
+ private SampleReader sampleReader;
+
+ // State that should not be reset on seek.
+ private boolean hasOutputFormat;
+
+ // State that should be reset on seek.
+ private final boolean[] prefixFlags;
+ private final NalUnitTargetBuffer vps;
+ private final NalUnitTargetBuffer sps;
+ private final NalUnitTargetBuffer pps;
+ private final NalUnitTargetBuffer prefixSei;
+ private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed?
+ private long totalBytesWritten;
+
+ // Per packet state that gets reset at the start of each packet.
+ private long pesTimeUs;
+
+ // Scratch variables to avoid allocations.
+ private final ParsableByteArray seiWrapper;
+
+ /**
+ * @param seiReader An SEI reader for consuming closed caption channels.
+ */
+ public H265Reader(SeiReader seiReader) {
+ this.seiReader = seiReader;
+ prefixFlags = new boolean[3];
+ vps = new NalUnitTargetBuffer(VPS_NUT, 128);
+ sps = new NalUnitTargetBuffer(SPS_NUT, 128);
+ pps = new NalUnitTargetBuffer(PPS_NUT, 128);
+ prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128);
+ suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128);
+ seiWrapper = new ParsableByteArray();
+ }
+
+ @Override
+ public void seek() {
+ NalUnitUtil.clearPrefixFlags(prefixFlags);
+ vps.reset();
+ sps.reset();
+ pps.reset();
+ prefixSei.reset();
+ suffixSei.reset();
+ sampleReader.reset();
+ totalBytesWritten = 0;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
+ sampleReader = new SampleReader(output);
+ seiReader.createTracks(extractorOutput, idGenerator);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ this.pesTimeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ int offset = data.getPosition();
+ int limit = data.limit();
+ byte[] dataArray = data.data;
+
+ // Append the data to the buffer.
+ totalBytesWritten += data.bytesLeft();
+ output.sampleData(data, data.bytesLeft());
+
+ // Scan the appended data, processing NAL units as they are encountered
+ while (offset < limit) {
+ int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
+
+ if (nalUnitOffset == limit) {
+ // We've scanned to the end of the data without finding the start of another NAL unit.
+ nalUnitData(dataArray, offset, limit);
+ return;
+ }
+
+ // We've seen the start of a NAL unit of the following type.
+ int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nalUnitOffset);
+
+ // This is the number of bytes from the current offset to the start of the next NAL unit.
+ // It may be negative if the NAL unit started in the previously consumed data.
+ int lengthToNalUnit = nalUnitOffset - offset;
+ if (lengthToNalUnit > 0) {
+ nalUnitData(dataArray, offset, nalUnitOffset);
+ }
+
+ int bytesWrittenPastPosition = limit - nalUnitOffset;
+ long absolutePosition = totalBytesWritten - bytesWrittenPastPosition;
+ // Indicate the end of the previous NAL unit. If the length to the start of the next unit
+ // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes
+ // when notifying that the unit has ended.
+ endNalUnit(absolutePosition, bytesWrittenPastPosition,
+ lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs);
+ // Indicate the start of the next NAL unit.
+ startNalUnit(absolutePosition, bytesWrittenPastPosition, nalUnitType, pesTimeUs);
+ // Continue scanning the data.
+ offset = nalUnitOffset + 3;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+ if (hasOutputFormat) {
+ sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);
+ } else {
+ vps.startNalUnit(nalUnitType);
+ sps.startNalUnit(nalUnitType);
+ pps.startNalUnit(nalUnitType);
+ }
+ prefixSei.startNalUnit(nalUnitType);
+ suffixSei.startNalUnit(nalUnitType);
+ }
+
+ private void nalUnitData(byte[] dataArray, int offset, int limit) {
+ if (hasOutputFormat) {
+ sampleReader.readNalUnitData(dataArray, offset, limit);
+ } else {
+ vps.appendToNalUnit(dataArray, offset, limit);
+ sps.appendToNalUnit(dataArray, offset, limit);
+ pps.appendToNalUnit(dataArray, offset, limit);
+ }
+ prefixSei.appendToNalUnit(dataArray, offset, limit);
+ suffixSei.appendToNalUnit(dataArray, offset, limit);
+ }
+
+ private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
+ if (hasOutputFormat) {
+ sampleReader.endNalUnit(position, offset);
+ } else {
+ vps.endNalUnit(discardPadding);
+ sps.endNalUnit(discardPadding);
+ pps.endNalUnit(discardPadding);
+ if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) {
+ output.format(parseMediaFormat(formatId, vps, sps, pps));
+ hasOutputFormat = true;
+ }
+ }
+ if (prefixSei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength);
+ seiWrapper.reset(prefixSei.nalData, unescapedLength);
+
+ // Skip the NAL prefix and type.
+ seiWrapper.skipBytes(5);
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ if (suffixSei.endNalUnit(discardPadding)) {
+ int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength);
+ seiWrapper.reset(suffixSei.nalData, unescapedLength);
+
+ // Skip the NAL prefix and type.
+ seiWrapper.skipBytes(5);
+ seiReader.consume(pesTimeUs, seiWrapper);
+ }
+ }
+
+ private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps,
+ NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
+ // Build codec-specific data.
+ byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
+ System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength);
+ System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength);
+ System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength);
+
+ // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1.
+ ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength);
+ bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id
+ int maxSubLayersMinus1 = bitArray.readBits(3);
+ bitArray.skipBits(1); // sps_temporal_id_nesting_flag
+
+ // profile_tier_level(1, sps_max_sub_layers_minus1)
+ bitArray.skipBits(88); // if (profilePresentFlag) {...}
+ bitArray.skipBits(8); // general_level_idc
+ int toSkip = 0;
+ for (int i = 0; i < maxSubLayersMinus1; i++) {
+ if (bitArray.readBit()) { // sub_layer_profile_present_flag[i]
+ toSkip += 89;
+ }
+ if (bitArray.readBit()) { // sub_layer_level_present_flag[i]
+ toSkip += 8;
+ }
+ }
+ bitArray.skipBits(toSkip);
+ if (maxSubLayersMinus1 > 0) {
+ bitArray.skipBits(2 * (8 - maxSubLayersMinus1));
+ }
+
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id
+ int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt();
+ if (chromaFormatIdc == 3) {
+ bitArray.skipBits(1); // separate_colour_plane_flag
+ }
+ int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();
+ int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt();
+ if (bitArray.readBit()) { // conformance_window_flag
+ int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt();
+ int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt();
+ // H.265/HEVC (2014) Table 6-1
+ int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1;
+ int subHeightC = chromaFormatIdc == 1 ? 2 : 1;
+ picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset);
+ picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset);
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+ bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+ int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt();
+ // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...)
+ for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i]
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i]
+ bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i]
+ }
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size
+ bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter
+ bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra
+ // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}}
+ boolean scalingListEnabled = bitArray.readBit();
+ if (scalingListEnabled && bitArray.readBit()) {
+ skipScalingList(bitArray);
+ }
+ bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1)
+ if (bitArray.readBit()) { // pcm_enabled_flag
+ // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4)
+ bitArray.skipBits(8);
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3
+ bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size
+ bitArray.skipBits(1); // pcm_loop_filter_disabled_flag
+ }
+ // Skips all short term reference picture sets.
+ skipShortTermRefPicSets(bitArray);
+ if (bitArray.readBit()) { // long_term_ref_pics_present_flag
+ // num_long_term_ref_pics_sps
+ for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) {
+ int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4;
+ // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i]
+ bitArray.skipBits(ltRefPicPocLsbSpsLength + 1);
+ }
+ }
+ bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag
+ float pixelWidthHeightRatio = 1;
+ if (bitArray.readBit()) { // vui_parameters_present_flag
+ if (bitArray.readBit()) { // aspect_ratio_info_present_flag
+ int aspectRatioIdc = bitArray.readBits(8);
+ if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {
+ int sarWidth = bitArray.readBits(16);
+ int sarHeight = bitArray.readBits(16);
+ if (sarWidth != 0 && sarHeight != 0) {
+ pixelWidthHeightRatio = (float) sarWidth / sarHeight;
+ }
+ } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {
+ pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
+ } else {
+ Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
+ }
+ }
+ }
+
+ return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE,
+ Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE,
+ Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null);
+ }
+
+ /**
+ * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4.
+ */
+ private static void skipScalingList(ParsableNalUnitBitArray bitArray) {
+ for (int sizeId = 0; sizeId < 4; sizeId++) {
+ for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) {
+ if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId]
+ // scaling_list_pred_matrix_id_delta[sizeId][matrixId]
+ bitArray.readUnsignedExpGolombCodedInt();
+ } else {
+ int coefNum = Math.min(64, 1 << (4 + (sizeId << 1)));
+ if (sizeId > 1) {
+ // scaling_list_dc_coef_minus8[sizeId - 2][matrixId]
+ bitArray.readSignedExpGolombCodedInt();
+ }
+ for (int i = 0; i < coefNum; i++) {
+ bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of
+ * them. See H.265/HEVC (2014) 7.3.7.
+ */
+ private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) {
+ int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();
+ boolean interRefPicSetPredictionFlag = false;
+ int numNegativePics;
+ int numPositivePics;
+ // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous
+ // one, so we just keep track of that rather than storing the whole array.
+ // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.
+ int previousNumDeltaPocs = 0;
+ for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
+ if (stRpsIdx != 0) {
+ interRefPicSetPredictionFlag = bitArray.readBit();
+ }
+ if (interRefPicSetPredictionFlag) {
+ bitArray.skipBits(1); // delta_rps_sign
+ bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1
+ for (int j = 0; j <= previousNumDeltaPocs; j++) {
+ if (bitArray.readBit()) { // used_by_curr_pic_flag[j]
+ bitArray.skipBits(1); // use_delta_flag[j]
+ }
+ }
+ } else {
+ numNegativePics = bitArray.readUnsignedExpGolombCodedInt();
+ numPositivePics = bitArray.readUnsignedExpGolombCodedInt();
+ previousNumDeltaPocs = numNegativePics + numPositivePics;
+ for (int i = 0; i < numNegativePics; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i]
+ bitArray.skipBits(1); // used_by_curr_pic_s0_flag[i]
+ }
+ for (int i = 0; i < numPositivePics; i++) {
+ bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]
+ bitArray.skipBits(1); // used_by_curr_pic_s1_flag[i]
+ }
+ }
+ }
+ }
+
+ private static final class SampleReader {
+
+ /**
+ * Offset in bytes of the first_slice_segment_in_pic_flag in a NAL unit containing a
+ * slice_segment_layer_rbsp.
+ */
+ private static final int FIRST_SLICE_FLAG_OFFSET = 2;
+
+ private final TrackOutput output;
+
+ // Per NAL unit state. A sample consists of one or more NAL units.
+ private long nalUnitStartPosition;
+ private boolean nalUnitHasKeyframeData;
+ private int nalUnitBytesRead;
+ private long nalUnitTimeUs;
+ private boolean lookingForFirstSliceFlag;
+ private boolean isFirstSlice;
+ private boolean isFirstParameterSet;
+
+ // Per sample state that gets reset at the start of each sample.
+ private boolean readingSample;
+ private boolean writingParameterSets;
+ private long samplePosition;
+ private long sampleTimeUs;
+ private boolean sampleIsKeyframe;
+
+ public SampleReader(TrackOutput output) {
+ this.output = output;
+ }
+
+ public void reset() {
+ lookingForFirstSliceFlag = false;
+ isFirstSlice = false;
+ isFirstParameterSet = false;
+ readingSample = false;
+ writingParameterSets = false;
+ }
+
+ public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+ isFirstSlice = false;
+ isFirstParameterSet = false;
+ nalUnitTimeUs = pesTimeUs;
+ nalUnitBytesRead = 0;
+ nalUnitStartPosition = position;
+
+ if (nalUnitType >= VPS_NUT) {
+ if (!writingParameterSets && readingSample) {
+ // This is a non-VCL NAL unit, so flush the previous sample.
+ outputSample(offset);
+ readingSample = false;
+ }
+ if (nalUnitType <= PPS_NUT) {
+ // This sample will have parameter sets at the start.
+ isFirstParameterSet = !writingParameterSets;
+ writingParameterSets = true;
+ }
+ }
+
+ // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp.
+ nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT);
+ lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R;
+ }
+
+ public void readNalUnitData(byte[] data, int offset, int limit) {
+ if (lookingForFirstSliceFlag) {
+ int headerOffset = offset + FIRST_SLICE_FLAG_OFFSET - nalUnitBytesRead;
+ if (headerOffset < limit) {
+ isFirstSlice = (data[headerOffset] & 0x80) != 0;
+ lookingForFirstSliceFlag = false;
+ } else {
+ nalUnitBytesRead += limit - offset;
+ }
+ }
+ }
+
+ public void endNalUnit(long position, int offset) {
+ if (writingParameterSets && isFirstSlice) {
+ // This sample has parameter sets. Reset the key-frame flag based on the first slice.
+ sampleIsKeyframe = nalUnitHasKeyframeData;
+ writingParameterSets = false;
+ } else if (isFirstParameterSet || isFirstSlice) {
+ // This NAL unit is at the start of a new sample (access unit).
+ if (readingSample) {
+ // Output the sample ending before this NAL unit.
+ int nalUnitLength = (int) (position - nalUnitStartPosition);
+ outputSample(offset + nalUnitLength);
+ }
+ samplePosition = nalUnitStartPosition;
+ sampleTimeUs = nalUnitTimeUs;
+ readingSample = true;
+ sampleIsKeyframe = nalUnitHasKeyframeData;
+ }
+ }
+
+ private void outputSample(int offset) {
+ @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ int size = (int) (nalUnitStartPosition - samplePosition);
+ output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses ID3 data and extracts individual text information frames.
+ */
+public final class Id3Reader implements ElementaryStreamReader {
+
+ private static final String TAG = "Id3Reader";
+
+ private static final int ID3_HEADER_SIZE = 10;
+
+ private final ParsableByteArray id3Header;
+
+ private TrackOutput output;
+
+ // State that should be reset on seek.
+ private boolean writingSample;
+
+ // Per sample state that gets reset at the start of each sample.
+ private long sampleTimeUs;
+ private int sampleSize;
+ private int sampleBytesRead;
+
+ public Id3Reader() {
+ id3Header = new ParsableByteArray(ID3_HEADER_SIZE);
+ }
+
+ @Override
+ public void seek() {
+ writingSample = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3,
+ null, Format.NO_VALUE, null));
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ if (!dataAlignmentIndicator) {
+ return;
+ }
+ writingSample = true;
+ sampleTimeUs = pesTimeUs;
+ sampleSize = 0;
+ sampleBytesRead = 0;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ if (!writingSample) {
+ return;
+ }
+ int bytesAvailable = data.bytesLeft();
+ if (sampleBytesRead < ID3_HEADER_SIZE) {
+ // We're still reading the ID3 header.
+ int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead);
+ System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead,
+ headerBytesAvailable);
+ if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) {
+ // We've finished reading the ID3 header. Extract the sample size.
+ id3Header.setPosition(0);
+ if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte()
+ || '3' != id3Header.readUnsignedByte()) {
+ Log.w(TAG, "Discarding invalid ID3 tag");
+ writingSample = false;
+ return;
+ }
+ id3Header.skipBytes(3); // version (2) + flags (1)
+ sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt();
+ }
+ }
+ // Write data to the output.
+ int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead);
+ output.sampleData(data, bytesToWrite);
+ sampleBytesRead += bytesToWrite;
+ }
+
+ @Override
+ public void packetFinished() {
+ if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {
+ return;
+ }
+ output.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ writingSample = false;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
@@ -0,0 +1,221 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous MPEG Audio byte stream and extracts individual frames.
+ */
+public final class MpegAudioReader implements ElementaryStreamReader {
+
+ private static final int STATE_FINDING_HEADER = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_FRAME = 2;
+
+ private static final int HEADER_SIZE = 4;
+
+ private final ParsableByteArray headerScratch;
+ private final MpegAudioHeader header;
+ private final String language;
+
+ private String formatId;
+ private TrackOutput output;
+
+ private int state;
+ private int frameBytesRead;
+ private boolean hasOutputFormat;
+
+ // Used when finding the frame header.
+ private boolean lastByteWasFF;
+
+ // Parsed from the frame header.
+ private long frameDurationUs;
+ private int frameSize;
+
+ // The timestamp to attach to the next sample in the current packet.
+ private long timeUs;
+
+ public MpegAudioReader() {
+ this(null);
+ }
+
+ public MpegAudioReader(String language) {
+ state = STATE_FINDING_HEADER;
+ // The first byte of an MPEG Audio frame header is always 0xFF.
+ headerScratch = new ParsableByteArray(4);
+ headerScratch.data[0] = (byte) 0xFF;
+ header = new MpegAudioHeader();
+ this.language = language;
+ }
+
+ @Override
+ public void seek() {
+ state = STATE_FINDING_HEADER;
+ frameBytesRead = 0;
+ lastByteWasFF = false;
+ }
+
+ @Override
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ idGenerator.generateNewId();
+ formatId = idGenerator.getFormatId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
+ }
+
+ @Override
+ public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ timeUs = pesTimeUs;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data) {
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ findHeader(data);
+ break;
+ case STATE_READING_HEADER:
+ readHeaderRemainder(data);
+ break;
+ case STATE_READING_FRAME:
+ readFrameRemainder(data);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void packetFinished() {
+ // Do nothing.
+ }
+
+ /**
+ * Attempts to locate the start of the next frame header.
+ * <p>
+ * If a frame header is located then the state is changed to {@link #STATE_READING_HEADER}, the
+ * first two bytes of the header are written into {@link #headerScratch}, and the position of the
+ * source is advanced to the byte that immediately follows these two bytes.
+ * <p>
+ * If a frame header is not located then the position of the source is advanced to the limit, and
+ * the method should be called again with the next source to continue the search.
+ *
+ * @param source The source from which to read.
+ */
+ private void findHeader(ParsableByteArray source) {
+ byte[] data = source.data;
+ int startOffset = source.getPosition();
+ int endOffset = source.limit();
+ for (int i = startOffset; i < endOffset; i++) {
+ boolean byteIsFF = (data[i] & 0xFF) == 0xFF;
+ boolean found = lastByteWasFF && (data[i] & 0xE0) == 0xE0;
+ lastByteWasFF = byteIsFF;
+ if (found) {
+ source.setPosition(i + 1);
+ // Reset lastByteWasFF for next time.
+ lastByteWasFF = false;
+ headerScratch.data[1] = data[i];
+ frameBytesRead = 2;
+ state = STATE_READING_HEADER;
+ return;
+ }
+ }
+ source.setPosition(endOffset);
+ }
+
+ /**
+ * Attempts to read the remaining two bytes of the frame header.
+ * <p>
+ * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},
+ * the media format is output if this has not previously occurred, the four header bytes are
+ * output as sample data, and the position of the source is advanced to the byte that immediately
+ * follows the header.
+ * <p>
+ * If a frame header is read in full but cannot be parsed then the state is changed to
+ * {@link #STATE_READING_HEADER}.
+ * <p>
+ * If a frame header is not read in full then the position of the source is advanced to the limit,
+ * and the method should be called again with the next source to continue the read.
+ *
+ * @param source The source from which to read.
+ */
+ private void readHeaderRemainder(ParsableByteArray source) {
+ int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead);
+ source.readBytes(headerScratch.data, frameBytesRead, bytesToRead);
+ frameBytesRead += bytesToRead;
+ if (frameBytesRead < HEADER_SIZE) {
+ // We haven't read the whole header yet.
+ return;
+ }
+
+ headerScratch.setPosition(0);
+ boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header);
+ if (!parsedHeader) {
+ // We thought we'd located a frame header, but we hadn't.
+ frameBytesRead = 0;
+ state = STATE_READING_HEADER;
+ return;
+ }
+
+ frameSize = header.frameSize;
+ if (!hasOutputFormat) {
+ frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate;
+ Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null,
+ Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate,
+ null, null, 0, language);
+ output.format(format);
+ hasOutputFormat = true;
+ }
+
+ headerScratch.setPosition(0);
+ output.sampleData(headerScratch, HEADER_SIZE);
+ state = STATE_READING_FRAME;
+ }
+
+ /**
+ * Attempts to read the remainder of the frame.
+ * <p>
+ * If a frame is read in full then true is returned. The frame will have been output, and the
+ * position of the source will have been advanced to the byte that immediately follows the end of
+ * the frame.
+ * <p>
+ * If a frame is not read in full then the position of the source will have been advanced to the
+ * limit, and the method should be called again with the next source to continue the read.
+ *
+ * @param source The source from which to read.
+ */
+ private void readFrameRemainder(ParsableByteArray source) {
+ int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead);
+ output.sampleData(source, bytesToRead);
+ frameBytesRead += bytesToRead;
+ if (frameBytesRead < frameSize) {
+ // We haven't read the whole of the frame yet.
+ return;
+ }
+
+ output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, frameSize, 0, null);
+ timeUs += frameDurationUs;
+ frameBytesRead = 0;
+ state = STATE_FINDING_HEADER;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java
@@ -0,0 +1,109 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+/**
+ * A buffer that fills itself with data corresponding to a specific NAL unit, as it is
+ * encountered in the stream.
+ */
+/* package */ final class NalUnitTargetBuffer {
+
+ private final int targetType;
+
+ private boolean isFilling;
+ private boolean isCompleted;
+
+ public byte[] nalData;
+ public int nalLength;
+
+ public NalUnitTargetBuffer(int targetType, int initialCapacity) {
+ this.targetType = targetType;
+
+ // Initialize data with a start code in the first three bytes.
+ nalData = new byte[3 + initialCapacity];
+ nalData[2] = 1;
+ }
+
+ /**
+ * Resets the buffer, clearing any data that it holds.
+ */
+ public void reset() {
+ isFilling = false;
+ isCompleted = false;
+ }
+
+ /**
+ * Returns whether the buffer currently holds a complete NAL unit of the target type.
+ */
+ public boolean isCompleted() {
+ return isCompleted;
+ }
+
+ /**
+ * Called to indicate that a NAL unit has started.
+ *
+ * @param type The type of the NAL unit.
+ */
+ public void startNalUnit(int type) {
+ Assertions.checkState(!isFilling);
+ isFilling = type == targetType;
+ if (isFilling) {
+ // Skip the three byte start code when writing data.
+ nalLength = 3;
+ isCompleted = false;
+ }
+ }
+
+ /**
+ * Called to pass stream data. The data passed should not include the 3 byte start code.
+ *
+ * @param data Holds the data being passed.
+ * @param offset The offset of the data in {@code data}.
+ * @param limit The limit (exclusive) of the data in {@code data}.
+ */
+ public void appendToNalUnit(byte[] data, int offset, int limit) {
+ if (!isFilling) {
+ return;
+ }
+ int readLength = limit - offset;
+ if (nalData.length < nalLength + readLength) {
+ nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2);
+ }
+ System.arraycopy(data, offset, nalData, nalLength, readLength);
+ nalLength += readLength;
+ }
+
+ /**
+ * Called to indicate that a NAL unit has ended.
+ *
+ * @param discardPadding The number of excess bytes that were passed to
+ * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded.
+ * @return Whether the ended NAL unit is of the target type.
+ */
+ public boolean endNalUnit(int discardPadding) {
+ if (!isFilling) {
+ return false;
+ }
+ nalLength -= discardPadding;
+ isFilling = false;
+ isCompleted = true;
+ return true;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
@@ -0,0 +1,235 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Parses PES packet data and extracts samples.
+ */
+public final class PesReader implements TsPayloadReader {
+
+ private static final String TAG = "PesReader";
+
+ private static final int STATE_FINDING_HEADER = 0;
+ private static final int STATE_READING_HEADER = 1;
+ private static final int STATE_READING_HEADER_EXTENSION = 2;
+ private static final int STATE_READING_BODY = 3;
+
+ private static final int HEADER_SIZE = 9;
+ private static final int MAX_HEADER_EXTENSION_SIZE = 10;
+ private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE)
+
+ private final ElementaryStreamReader reader;
+ private final ParsableBitArray pesScratch;
+
+ private int state;
+ private int bytesRead;
+
+ private TimestampAdjuster timestampAdjuster;
+ private boolean ptsFlag;
+ private boolean dtsFlag;
+ private boolean seenFirstDts;
+ private int extendedHeaderLength;
+ private int payloadSize;
+ private boolean dataAlignmentIndicator;
+ private long timeUs;
+
+ public PesReader(ElementaryStreamReader reader) {
+ this.reader = reader;
+ pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);
+ state = STATE_FINDING_HEADER;
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ this.timestampAdjuster = timestampAdjuster;
+ reader.createTracks(extractorOutput, idGenerator);
+ }
+
+ // TsPayloadReader implementation.
+
+ @Override
+ public final void seek() {
+ state = STATE_FINDING_HEADER;
+ bytesRead = 0;
+ seenFirstDts = false;
+ reader.seek();
+ }
+
+ @Override
+ public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
+ if (payloadUnitStartIndicator) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ case STATE_READING_HEADER:
+ // Expected.
+ break;
+ case STATE_READING_HEADER_EXTENSION:
+ Log.w(TAG, "Unexpected start indicator reading extended header");
+ break;
+ case STATE_READING_BODY:
+ // If payloadSize == -1 then the length of the previous packet was unspecified, and so
+ // we only know that it's finished now that we've seen the start of the next one. This
+ // is expected. If payloadSize != -1, then the length of the previous packet was known,
+ // but we didn't receive that amount of data. This is not expected.
+ if (payloadSize != -1) {
+ Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes");
+ }
+ // Either way, notify the reader that it has now finished.
+ reader.packetFinished();
+ break;
+ }
+ setState(STATE_READING_HEADER);
+ }
+
+ while (data.bytesLeft() > 0) {
+ switch (state) {
+ case STATE_FINDING_HEADER:
+ data.skipBytes(data.bytesLeft());
+ break;
+ case STATE_READING_HEADER:
+ if (continueRead(data, pesScratch.data, HEADER_SIZE)) {
+ setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER);
+ }
+ break;
+ case STATE_READING_HEADER_EXTENSION:
+ int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);
+ // Read as much of the extended header as we're interested in, and skip the rest.
+ if (continueRead(data, pesScratch.data, readLength)
+ && continueRead(data, null, extendedHeaderLength)) {
+ parseHeaderExtension();
+ reader.packetStarted(timeUs, dataAlignmentIndicator);
+ setState(STATE_READING_BODY);
+ }
+ break;
+ case STATE_READING_BODY:
+ readLength = data.bytesLeft();
+ int padding = payloadSize == -1 ? 0 : readLength - payloadSize;
+ if (padding > 0) {
+ readLength -= padding;
+ data.setLimit(data.getPosition() + readLength);
+ }
+ reader.consume(data);
+ if (payloadSize != -1) {
+ payloadSize -= readLength;
+ if (payloadSize == 0) {
+ reader.packetFinished();
+ setState(STATE_READING_HEADER);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ private void setState(int state) {
+ this.state = state;
+ bytesRead = 0;
+ }
+
+ /**
+ * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+ * that the data should be written into {@code target} starting from an offset of zero.
+ *
+ * @param source The source from which to read.
+ * @param target The target into which data is to be read, or {@code null} to skip.
+ * @param targetLength The target length of the read.
+ * @return Whether the target length has been reached.
+ */
+ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+ int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+ if (bytesToRead <= 0) {
+ return true;
+ } else if (target == null) {
+ source.skipBytes(bytesToRead);
+ } else {
+ source.readBytes(target, bytesRead, bytesToRead);
+ }
+ bytesRead += bytesToRead;
+ return bytesRead == targetLength;
+ }
+
+ private boolean parseHeader() {
+ // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
+ // the header.
+ pesScratch.setPosition(0);
+ int startCodePrefix = pesScratch.readBits(24);
+ if (startCodePrefix != 0x000001) {
+ Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);
+ payloadSize = -1;
+ return false;
+ }
+
+ pesScratch.skipBits(8); // stream_id.
+ int packetLength = pesScratch.readBits(16);
+ pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1)
+ dataAlignmentIndicator = pesScratch.readBit();
+ pesScratch.skipBits(2); // copyright (1), original_or_copy (1)
+ ptsFlag = pesScratch.readBit();
+ dtsFlag = pesScratch.readBit();
+ // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),
+ // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)
+ pesScratch.skipBits(6);
+ extendedHeaderLength = pesScratch.readBits(8);
+
+ if (packetLength == 0) {
+ payloadSize = -1;
+ } else {
+ payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */
+ - HEADER_SIZE - extendedHeaderLength;
+ }
+ return true;
+ }
+
+ private void parseHeaderExtension() {
+ pesScratch.setPosition(0);
+ timeUs = C.TIME_UNSET;
+ if (ptsFlag) {
+ pesScratch.skipBits(4); // '0010' or '0011'
+ long pts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ if (!seenFirstDts && dtsFlag) {
+ pesScratch.skipBits(4); // '0011'
+ long dts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ // Subsequent PES packets may have earlier presentation timestamps than this one, but they
+ // should all be greater than or equal to this packet's decode timestamp. We feed the
+ // decode timestamp to the adjuster here so that in the case that this is the first to be
+ // fed, the adjuster will be able to compute an offset to apply such that the adjusted
+ // presentation timestamps of all future packets are non-negative.
+ timestampAdjuster.adjustTsTimestamp(dts);
+ seenFirstDts = true;
+ }
+ timeUs = timestampAdjuster.adjustTsTimestamp(pts);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -0,0 +1,338 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of data from the MPEG-2 TS container format.
+ */
+public final class PsExtractor implements Extractor {
+
+ /**
+ * Factory for {@link PsExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new PsExtractor()};
+ }
+
+ };
+
+ private static final int PACK_START_CODE = 0x000001BA;
+ private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
+ private static final int PACKET_START_CODE_PREFIX = 0x000001;
+ private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
+ private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
+ private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
+
+ public static final int PRIVATE_STREAM_1 = 0xBD;
+ public static final int AUDIO_STREAM = 0xC0;
+ public static final int AUDIO_STREAM_MASK = 0xE0;
+ public static final int VIDEO_STREAM = 0xE0;
+ public static final int VIDEO_STREAM_MASK = 0xF0;
+
+ private final TimestampAdjuster timestampAdjuster;
+ private final SparseArray<PesReader> psPayloadReaders; // Indexed by pid
+ private final ParsableByteArray psPacketBuffer;
+ private boolean foundAllTracks;
+ private boolean foundAudioTrack;
+ private boolean foundVideoTrack;
+
+ // Accessed only by the loading thread.
+ private ExtractorOutput output;
+
+ public PsExtractor() {
+ this(new TimestampAdjuster(0));
+ }
+
+ public PsExtractor(TimestampAdjuster timestampAdjuster) {
+ this.timestampAdjuster = timestampAdjuster;
+ psPacketBuffer = new ParsableByteArray(4096);
+ psPayloadReaders = new SparseArray<>();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] scratch = new byte[14];
+ input.peekFully(scratch, 0, 14);
+
+ // Verify the PACK_START_CODE for the first 4 bytes
+ if (PACK_START_CODE != (((scratch[0] & 0xFF) << 24) | ((scratch[1] & 0xFF) << 16)
+ | ((scratch[2] & 0xFF) << 8) | (scratch[3] & 0xFF))) {
+ return false;
+ }
+ // Verify the 01xxx1xx marker on the 5th byte
+ if ((scratch[4] & 0xC4) != 0x44) {
+ return false;
+ }
+ // Verify the xxxxx1xx marker on the 7th byte
+ if ((scratch[6] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxx1xx marker on the 9th byte
+ if ((scratch[8] & 0x04) != 0x04) {
+ return false;
+ }
+ // Verify the xxxxxxx1 marker on the 10th byte
+ if ((scratch[9] & 0x01) != 0x01) {
+ return false;
+ }
+ // Verify the xxxxxx11 marker on the 13th byte
+ if ((scratch[12] & 0x03) != 0x03) {
+ return false;
+ }
+ // Read the stuffing length from the 14th byte (last 3 bits)
+ int packStuffingLength = scratch[13] & 0x07;
+ input.advancePeekPosition(packStuffingLength);
+ // Now check that the next 3 bytes are the beginning of an MPEG start code
+ input.peekFully(scratch, 0, 3);
+ return (PACKET_START_CODE_PREFIX == (((scratch[0] & 0xFF) << 16) | ((scratch[1] & 0xFF) << 8)
+ | (scratch[2] & 0xFF)));
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ timestampAdjuster.reset();
+ for (int i = 0; i < psPayloadReaders.size(); i++) {
+ psPayloadReaders.valueAt(i).seek();
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ // First peek and check what type of start code is next.
+ if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ psPacketBuffer.setPosition(0);
+ int nextStartCode = psPacketBuffer.readInt();
+ if (nextStartCode == MPEG_PROGRAM_END_CODE) {
+ return RESULT_END_OF_INPUT;
+ } else if (nextStartCode == PACK_START_CODE) {
+ // Now peek the rest of the pack_header.
+ input.peekFully(psPacketBuffer.data, 0, 10);
+
+ // We only care about the pack_stuffing_length in here, skip the first 77 bits.
+ psPacketBuffer.setPosition(9);
+
+ // Last 3 bits is the length.
+ int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07;
+
+ // Now skip the stuffing and the pack header.
+ input.skipFully(packStuffingLength + 14);
+ return RESULT_CONTINUE;
+ } else if (nextStartCode == SYSTEM_HEADER_START_CODE) {
+ // We just skip all this, but we need to get the length first.
+ input.peekFully(psPacketBuffer.data, 0, 2);
+
+ // Length is the next 2 bytes.
+ psPacketBuffer.setPosition(0);
+ int systemHeaderLength = psPacketBuffer.readUnsignedShort();
+ input.skipFully(systemHeaderLength + 6);
+ return RESULT_CONTINUE;
+ } else if (((nextStartCode & 0xFFFFFF00) >> 8) != PACKET_START_CODE_PREFIX) {
+ input.skipFully(1); // Skip bytes until we see a valid start code again.
+ return RESULT_CONTINUE;
+ }
+
+ // We're at the start of a regular PES packet now.
+ // Get the stream ID off the last byte of the start code.
+ int streamId = nextStartCode & 0xFF;
+
+ // Check to see if we have this one in our map yet, and if not, then add it.
+ PesReader payloadReader = psPayloadReaders.get(streamId);
+ if (!foundAllTracks) {
+ if (payloadReader == null) {
+ ElementaryStreamReader elementaryStreamReader = null;
+ if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) {
+ // Private stream, used for AC3 audio.
+ // NOTE: This may need further parsing to determine if its DTS, but that's likely only
+ // valid for DVDs.
+ elementaryStreamReader = new Ac3Reader();
+ foundAudioTrack = true;
+ } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
+ elementaryStreamReader = new MpegAudioReader();
+ foundAudioTrack = true;
+ } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
+ elementaryStreamReader = new H262Reader();
+ foundVideoTrack = true;
+ }
+ if (elementaryStreamReader != null) {
+ TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
+ elementaryStreamReader.createTracks(output, idGenerator);
+ payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
+ psPayloadReaders.put(streamId, payloadReader);
+ }
+ }
+ if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) {
+ foundAllTracks = true;
+ output.endTracks();
+ }
+ }
+
+ // The next 2 bytes are the length. Once we have that we can consume the complete packet.
+ input.peekFully(psPacketBuffer.data, 0, 2);
+ psPacketBuffer.setPosition(0);
+ int payloadLength = psPacketBuffer.readUnsignedShort();
+ int pesLength = payloadLength + 6;
+
+ if (payloadReader == null) {
+ // Just skip this data.
+ input.skipFully(pesLength);
+ } else {
+ psPacketBuffer.reset(pesLength);
+ // Read the whole packet and the header for consumption.
+ input.readFully(psPacketBuffer.data, 0, pesLength);
+ psPacketBuffer.setPosition(6);
+ payloadReader.consume(psPacketBuffer);
+ psPacketBuffer.setLimit(psPacketBuffer.capacity());
+ }
+
+ return RESULT_CONTINUE;
+ }
+
+ // Internals.
+
+ /**
+ * Parses PES packet data and extracts samples.
+ */
+ private static final class PesReader {
+
+ private static final int PES_SCRATCH_SIZE = 64;
+
+ private final ElementaryStreamReader pesPayloadReader;
+ private final TimestampAdjuster timestampAdjuster;
+ private final ParsableBitArray pesScratch;
+
+ private boolean ptsFlag;
+ private boolean dtsFlag;
+ private boolean seenFirstDts;
+ private int extendedHeaderLength;
+ private long timeUs;
+
+ public PesReader(ElementaryStreamReader pesPayloadReader, TimestampAdjuster timestampAdjuster) {
+ this.pesPayloadReader = pesPayloadReader;
+ this.timestampAdjuster = timestampAdjuster;
+ pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]);
+ }
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray)} will not be a continuation of the data that was
+ * previously passed. Hence the reader should reset any internal state.
+ */
+ public void seek() {
+ seenFirstDts = false;
+ pesPayloadReader.seek();
+ }
+
+ /**
+ * Consumes the payload of a PS packet.
+ *
+ * @param data The PES packet. The position will be set to the start of the payload.
+ */
+ public void consume(ParsableByteArray data) {
+ data.readBytes(pesScratch.data, 0, 3);
+ pesScratch.setPosition(0);
+ parseHeader();
+ data.readBytes(pesScratch.data, 0, extendedHeaderLength);
+ pesScratch.setPosition(0);
+ parseHeaderExtension();
+ pesPayloadReader.packetStarted(timeUs, true);
+ pesPayloadReader.consume(data);
+ // We always have complete PES packets with program stream.
+ pesPayloadReader.packetFinished();
+ }
+
+ private void parseHeader() {
+ // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
+ // the header.
+ // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1),
+ // data_alignment_indicator (1), copyright (1), original_or_copy (1)
+ pesScratch.skipBits(8);
+ ptsFlag = pesScratch.readBit();
+ dtsFlag = pesScratch.readBit();
+ // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1),
+ // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1)
+ pesScratch.skipBits(6);
+ extendedHeaderLength = pesScratch.readBits(8);
+ }
+
+ private void parseHeaderExtension() {
+ timeUs = 0;
+ if (ptsFlag) {
+ pesScratch.skipBits(4); // '0010' or '0011'
+ long pts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ pts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ if (!seenFirstDts && dtsFlag) {
+ pesScratch.skipBits(4); // '0011'
+ long dts = (long) pesScratch.readBits(3) << 30;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15) << 15;
+ pesScratch.skipBits(1); // marker_bit
+ dts |= pesScratch.readBits(15);
+ pesScratch.skipBits(1); // marker_bit
+ // Subsequent PES packets may have earlier presentation timestamps than this one, but they
+ // should all be greater than or equal to this packet's decode timestamp. We feed the
+ // decode timestamp to the adjuster here so that in the case that this is the first to be
+ // fed, the adjuster will be able to compute an offset to apply such that the adjusted
+ // presentation timestamps of all future packets are non-negative.
+ timestampAdjuster.adjustTsTimestamp(dts);
+ seenFirstDts = true;
+ }
+ timeUs = timestampAdjuster.adjustTsTimestamp(pts);
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Reads section data.
+ */
+public interface SectionPayloadReader {
+
+ /**
+ * Initializes the section payload reader.
+ *
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator);
+
+ /**
+ * Called by a {@link SectionReader} when a full section is received.
+ *
+ * @param sectionData The data belonging to a section starting from the table_id. If
+ * section_syntax_indicator is set to '1', {@code sectionData} excludes the CRC_32 field.
+ * Otherwise, all bytes belonging to the table section are included.
+ */
+ void consume(ParsableByteArray sectionData);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java
@@ -0,0 +1,133 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Reads section data packets and feeds the whole sections to a given {@link SectionPayloadReader}.
+ * Useful information on PSI sections can be found in ISO/IEC 13818-1, section 2.4.4.
+ */
+public final class SectionReader implements TsPayloadReader {
+
+ private static final int SECTION_HEADER_LENGTH = 3;
+ private static final int DEFAULT_SECTION_BUFFER_LENGTH = 32;
+ private static final int MAX_SECTION_LENGTH = 4098;
+
+ private final SectionPayloadReader reader;
+ private final ParsableByteArray sectionData;
+
+ private int totalSectionLength;
+ private int bytesRead;
+ private boolean sectionSyntaxIndicator;
+ private boolean waitingForPayloadStart;
+
+ public SectionReader(SectionPayloadReader reader) {
+ this.reader = reader;
+ sectionData = new ParsableByteArray(DEFAULT_SECTION_BUFFER_LENGTH);
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ reader.init(timestampAdjuster, extractorOutput, idGenerator);
+ waitingForPayloadStart = true;
+ }
+
+ @Override
+ public void seek() {
+ waitingForPayloadStart = true;
+ }
+
+ @Override
+ public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
+ int payloadStartPosition = C.POSITION_UNSET;
+ if (payloadUnitStartIndicator) {
+ int payloadStartOffset = data.readUnsignedByte();
+ payloadStartPosition = data.getPosition() + payloadStartOffset;
+ }
+
+ if (waitingForPayloadStart) {
+ if (!payloadUnitStartIndicator) {
+ return;
+ }
+ waitingForPayloadStart = false;
+ data.setPosition(payloadStartPosition);
+ bytesRead = 0;
+ }
+
+ while (data.bytesLeft() > 0) {
+ if (bytesRead < SECTION_HEADER_LENGTH) {
+ // Note: see ISO/IEC 13818-1, section 2.4.4.3 for detailed information on the format of
+ // the header.
+ if (bytesRead == 0) {
+ int tableId = data.readUnsignedByte();
+ data.setPosition(data.getPosition() - 1);
+ if (tableId == 0xFF /* forbidden value */) {
+ // No more sections in this ts packet.
+ waitingForPayloadStart = true;
+ return;
+ }
+ }
+ int headerBytesToRead = Math.min(data.bytesLeft(), SECTION_HEADER_LENGTH - bytesRead);
+ data.readBytes(sectionData.data, bytesRead, headerBytesToRead);
+ bytesRead += headerBytesToRead;
+ if (bytesRead == SECTION_HEADER_LENGTH) {
+ sectionData.reset(SECTION_HEADER_LENGTH);
+ sectionData.skipBytes(1); // Skip table id (8).
+ int secondHeaderByte = sectionData.readUnsignedByte();
+ int thirdHeaderByte = sectionData.readUnsignedByte();
+ sectionSyntaxIndicator = (secondHeaderByte & 0x80) != 0;
+ totalSectionLength =
+ (((secondHeaderByte & 0x0F) << 8) | thirdHeaderByte) + SECTION_HEADER_LENGTH;
+ if (sectionData.capacity() < totalSectionLength) {
+ // Ensure there is enough space to keep the whole section.
+ byte[] bytes = sectionData.data;
+ sectionData.reset(
+ Math.min(MAX_SECTION_LENGTH, Math.max(totalSectionLength, bytes.length * 2)));
+ System.arraycopy(bytes, 0, sectionData.data, 0, SECTION_HEADER_LENGTH);
+ }
+ }
+ } else {
+ // Reading the body.
+ int bodyBytesToRead = Math.min(data.bytesLeft(), totalSectionLength - bytesRead);
+ data.readBytes(sectionData.data, bytesRead, bodyBytesToRead);
+ bytesRead += bodyBytesToRead;
+ if (bytesRead == totalSectionLength) {
+ if (sectionSyntaxIndicator) {
+ // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11.
+ if (Util.crc(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) {
+ // The CRC is invalid so discard the section.
+ waitingForPayloadStart = true;
+ return;
+ }
+ sectionData.reset(totalSectionLength - 4); // Exclude the CRC_32 field.
+ } else {
+ // This is a private section with private defined syntax.
+ sectionData.reset(totalSectionLength);
+ }
+ reader.consume(sectionData);
+ bytesRead = 0;
+ }
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.text.cea.CeaUtil;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.List;
+
+/**
+ * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}.
+ */
+/* package */ final class SeiReader {
+
+ private final List<Format> closedCaptionFormats;
+ private final TrackOutput[] outputs;
+
+ /**
+ * @param closedCaptionFormats A list of formats for the closed caption channels to expose.
+ */
+ public SeiReader(List<Format> closedCaptionFormats) {
+ this.closedCaptionFormats = closedCaptionFormats;
+ outputs = new TrackOutput[closedCaptionFormats.size()];
+ }
+
+ public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+ for (int i = 0; i < outputs.length; i++) {
+ idGenerator.generateNewId();
+ TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
+ Format channelFormat = closedCaptionFormats.get(i);
+ String channelMimeType = channelFormat.sampleMimeType;
+ Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
+ "Invalid closed caption mime type provided: " + channelMimeType);
+ output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null,
+ Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language,
+ channelFormat.accessibilityChannel, null));
+ outputs[i] = output;
+ }
+ }
+
+ public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
+ CeaUtil.consume(pesTimeUs, seiBuffer, outputs);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Parses splice info sections as defined by SCTE35.
+ */
+public final class SpliceInfoSectionReader implements SectionPayloadReader {
+
+ private TimestampAdjuster timestampAdjuster;
+ private TrackOutput output;
+ private boolean formatDeclared;
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TsPayloadReader.TrackIdGenerator idGenerator) {
+ this.timestampAdjuster = timestampAdjuster;
+ idGenerator.generateNewId();
+ output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+ output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35,
+ null, Format.NO_VALUE, null));
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ if (!formatDeclared) {
+ if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
+ // There is not enough information to initialize the timestamp adjuster.
+ return;
+ }
+ output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35,
+ timestampAdjuster.getTimestampOffsetUs()));
+ formatDeclared = true;
+ }
+ int sampleSize = sectionData.bytesLeft();
+ output.sampleData(sectionData, sampleSize);
+ output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,
+ sampleSize, 0, null);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -0,0 +1,538 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.support.annotation.IntDef;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.Flags;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.DvbSubtitleInfo;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Facilitates the extraction of data from the MPEG-2 TS container format.
+ */
+public final class TsExtractor implements Extractor {
+
+ /**
+ * Factory for {@link TsExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new TsExtractor()};
+ }
+
+ };
+
+ /**
+ * Modes for the extractor.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS})
+ public @interface Mode {}
+
+ /**
+ * Behave as defined in ISO/IEC 13818-1.
+ */
+ public static final int MODE_NORMAL = 0;
+ /**
+ * Assume only one PMT will be contained in the stream, even if more are declared by the PAT.
+ */
+ public static final int MODE_SINGLE_PMT = 1;
+ /**
+ * Enable single PMT mode, map {@link TrackOutput}s by their type (instead of PID) and ignore
+ * continuity counters.
+ */
+ public static final int MODE_HLS = 2;
+
+ public static final int TS_STREAM_TYPE_MPA = 0x03;
+ public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
+ public static final int TS_STREAM_TYPE_AAC = 0x0F;
+ public static final int TS_STREAM_TYPE_AC3 = 0x81;
+ public static final int TS_STREAM_TYPE_DTS = 0x8A;
+ public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82;
+ public static final int TS_STREAM_TYPE_E_AC3 = 0x87;
+ public static final int TS_STREAM_TYPE_H262 = 0x02;
+ public static final int TS_STREAM_TYPE_H264 = 0x1B;
+ public static final int TS_STREAM_TYPE_H265 = 0x24;
+ public static final int TS_STREAM_TYPE_ID3 = 0x15;
+ public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86;
+ public static final int TS_STREAM_TYPE_DVBSUBS = 0x59;
+
+ private static final int TS_PACKET_SIZE = 188;
+ private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
+ private static final int TS_PAT_PID = 0;
+ private static final int MAX_PID_PLUS_ONE = 0x2000;
+
+ private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3");
+ private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
+ private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC");
+
+ private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
+ private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
+
+ @Mode private final int mode;
+ private final List<TimestampAdjuster> timestampAdjusters;
+ private final ParsableByteArray tsPacketBuffer;
+ private final ParsableBitArray tsScratch;
+ private final SparseIntArray continuityCounters;
+ private final TsPayloadReader.Factory payloadReaderFactory;
+ private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
+ private final SparseBooleanArray trackIds;
+
+ // Accessed only by the loading thread.
+ private ExtractorOutput output;
+ private int remainingPmts;
+ private boolean tracksEnded;
+ private TsPayloadReader id3Reader;
+
+ public TsExtractor() {
+ this(0);
+ }
+
+ /**
+ * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
+ * {@code FLAG_*} values that control the behavior of the payload readers.
+ */
+ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
+ this(MODE_NORMAL, new TimestampAdjuster(0),
+ new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
+ }
+
+ /**
+ * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT}
+ * and {@link #MODE_HLS}.
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param payloadReaderFactory Factory for injecting a custom set of payload readers.
+ */
+ public TsExtractor(@Mode int mode, TimestampAdjuster timestampAdjuster,
+ TsPayloadReader.Factory payloadReaderFactory) {
+ this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
+ this.mode = mode;
+ if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {
+ timestampAdjusters = Collections.singletonList(timestampAdjuster);
+ } else {
+ timestampAdjusters = new ArrayList<>();
+ timestampAdjusters.add(timestampAdjuster);
+ }
+ tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
+ tsScratch = new ParsableBitArray(new byte[3]);
+ trackIds = new SparseBooleanArray();
+ tsPayloadReaders = new SparseArray<>();
+ continuityCounters = new SparseIntArray();
+ resetPayloadReaders();
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ byte[] buffer = tsPacketBuffer.data;
+ input.peekFully(buffer, 0, BUFFER_SIZE);
+ for (int j = 0; j < TS_PACKET_SIZE; j++) {
+ for (int i = 0; true; i++) {
+ if (i == BUFFER_PACKET_COUNT) {
+ input.skipFully(j);
+ return true;
+ }
+ if (buffer[j + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
+ break;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ int timestampAdjustersCount = timestampAdjusters.size();
+ for (int i = 0; i < timestampAdjustersCount; i++) {
+ timestampAdjusters.get(i).reset();
+ }
+ tsPacketBuffer.reset();
+ continuityCounters.clear();
+ // Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
+ resetPayloadReaders();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ byte[] data = tsPacketBuffer.data;
+ // Shift bytes to the start of the buffer if there isn't enough space left at the end
+ if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
+ int bytesLeft = tsPacketBuffer.bytesLeft();
+ if (bytesLeft > 0) {
+ System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);
+ }
+ tsPacketBuffer.reset(data, bytesLeft);
+ }
+ // Read more bytes until there is at least one packet size
+ while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
+ int limit = tsPacketBuffer.limit();
+ int read = input.read(data, limit, BUFFER_SIZE - limit);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return RESULT_END_OF_INPUT;
+ }
+ tsPacketBuffer.setLimit(limit + read);
+ }
+
+ // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of
+ // the header.
+ final int limit = tsPacketBuffer.limit();
+ int position = tsPacketBuffer.getPosition();
+ while (position < limit && data[position] != TS_SYNC_BYTE) {
+ position++;
+ }
+ tsPacketBuffer.setPosition(position);
+
+ int endOfPacket = position + TS_PACKET_SIZE;
+ if (endOfPacket > limit) {
+ return RESULT_CONTINUE;
+ }
+
+ tsPacketBuffer.skipBytes(1);
+ tsPacketBuffer.readBytes(tsScratch, 3);
+ if (tsScratch.readBit()) { // transport_error_indicator
+ // There are uncorrectable errors in this packet.
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ }
+ boolean payloadUnitStartIndicator = tsScratch.readBit();
+ tsScratch.skipBits(1); // transport_priority
+ int pid = tsScratch.readBits(13);
+ tsScratch.skipBits(2); // transport_scrambling_control
+ boolean adaptationFieldExists = tsScratch.readBit();
+ boolean payloadExists = tsScratch.readBit();
+
+ // Discontinuity check.
+ boolean discontinuityFound = false;
+ int continuityCounter = tsScratch.readBits(4);
+ if (mode != MODE_HLS) {
+ int previousCounter = continuityCounters.get(pid, continuityCounter - 1);
+ continuityCounters.put(pid, continuityCounter);
+ if (previousCounter == continuityCounter) {
+ if (payloadExists) {
+ // Duplicate packet found.
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ }
+ } else if (continuityCounter != (previousCounter + 1) % 16) {
+ discontinuityFound = true;
+ }
+ }
+
+ // Skip the adaptation field.
+ if (adaptationFieldExists) {
+ int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
+ tsPacketBuffer.skipBytes(adaptationFieldLength);
+ }
+
+ // Read the payload.
+ if (payloadExists) {
+ TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
+ if (payloadReader != null) {
+ if (discontinuityFound) {
+ payloadReader.seek();
+ }
+ tsPacketBuffer.setLimit(endOfPacket);
+ payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
+ Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket);
+ tsPacketBuffer.setLimit(limit);
+ }
+ }
+
+ tsPacketBuffer.setPosition(endOfPacket);
+ return RESULT_CONTINUE;
+ }
+
+ // Internals.
+
+ private void resetPayloadReaders() {
+ trackIds.clear();
+ tsPayloadReaders.clear();
+ SparseArray<TsPayloadReader> initialPayloadReaders =
+ payloadReaderFactory.createInitialPayloadReaders();
+ int initialPayloadReadersSize = initialPayloadReaders.size();
+ for (int i = 0; i < initialPayloadReadersSize; i++) {
+ tsPayloadReaders.put(initialPayloadReaders.keyAt(i), initialPayloadReaders.valueAt(i));
+ }
+ tsPayloadReaders.put(TS_PAT_PID, new SectionReader(new PatReader()));
+ id3Reader = null;
+ }
+
+ /**
+ * Parses Program Association Table data.
+ */
+ private class PatReader implements SectionPayloadReader {
+
+ private final ParsableBitArray patScratch;
+
+ public PatReader() {
+ patScratch = new ParsableBitArray(new byte[4]);
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ // Do nothing.
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ int tableId = sectionData.readUnsignedByte();
+ if (tableId != 0x00 /* program_association_section */) {
+ // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
+ return;
+ }
+ // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12),
+ // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1),
+ // section_number (8), last_section_number (8)
+ sectionData.skipBytes(7);
+
+ int programCount = sectionData.bytesLeft() / 4;
+ for (int i = 0; i < programCount; i++) {
+ sectionData.readBytes(patScratch, 4);
+ int programNumber = patScratch.readBits(16);
+ patScratch.skipBits(3); // reserved (3)
+ if (programNumber == 0) {
+ patScratch.skipBits(13); // network_PID (13)
+ } else {
+ int pid = patScratch.readBits(13);
+ tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));
+ remainingPmts++;
+ }
+ }
+ if (mode != MODE_HLS) {
+ tsPayloadReaders.remove(TS_PAT_PID);
+ }
+ }
+
+ }
+
+ /**
+ * Parses Program Map Table.
+ */
+ private class PmtReader implements SectionPayloadReader {
+
+ private static final int TS_PMT_DESC_REGISTRATION = 0x05;
+ private static final int TS_PMT_DESC_ISO639_LANG = 0x0A;
+ private static final int TS_PMT_DESC_AC3 = 0x6A;
+ private static final int TS_PMT_DESC_EAC3 = 0x7A;
+ private static final int TS_PMT_DESC_DTS = 0x7B;
+ private static final int TS_PMT_DESC_DVBSUBS = 0x59;
+
+ private final ParsableBitArray pmtScratch;
+ private final int pid;
+
+ public PmtReader(int pid) {
+ pmtScratch = new ParsableBitArray(new byte[5]);
+ this.pid = pid;
+ }
+
+ @Override
+ public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator) {
+ // Do nothing.
+ }
+
+ @Override
+ public void consume(ParsableByteArray sectionData) {
+ int tableId = sectionData.readUnsignedByte();
+ if (tableId != 0x02 /* TS_program_map_section */) {
+ // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
+ return;
+ }
+ // TimestampAdjuster assignment.
+ TimestampAdjuster timestampAdjuster;
+ if (mode == MODE_SINGLE_PMT || mode == MODE_HLS || remainingPmts == 1) {
+ timestampAdjuster = timestampAdjusters.get(0);
+ } else {
+ timestampAdjuster = new TimestampAdjuster(
+ timestampAdjusters.get(0).getFirstSampleTimestampUs());
+ timestampAdjusters.add(timestampAdjuster);
+ }
+
+ // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12)
+ sectionData.skipBytes(2);
+ int programNumber = sectionData.readUnsignedShort();
+ // reserved (2), version_number (5), current_next_indicator (1), section_number (8),
+ // last_section_number (8), reserved (3), PCR_PID (13)
+ sectionData.skipBytes(5);
+
+ // Read program_info_length.
+ sectionData.readBytes(pmtScratch, 2);
+ pmtScratch.skipBits(4);
+ int programInfoLength = pmtScratch.readBits(12);
+
+ // Skip the descriptors.
+ sectionData.skipBytes(programInfoLength);
+
+ if (mode == MODE_HLS && id3Reader == null) {
+ // Setup an ID3 track regardless of whether there's a corresponding entry, in case one
+ // appears intermittently during playback. See [Internal: b/20261500].
+ EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, null, new byte[0]);
+ id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
+ id3Reader.init(timestampAdjuster, output,
+ new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
+ }
+
+ int remainingEntriesLength = sectionData.bytesLeft();
+ while (remainingEntriesLength > 0) {
+ sectionData.readBytes(pmtScratch, 5);
+ int streamType = pmtScratch.readBits(8);
+ pmtScratch.skipBits(3); // reserved
+ int elementaryPid = pmtScratch.readBits(13);
+ pmtScratch.skipBits(4); // reserved
+ int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
+ EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
+ if (streamType == 0x06) {
+ streamType = esInfo.streamType;
+ }
+ remainingEntriesLength -= esInfoLength + 5;
+
+ int trackId = mode == MODE_HLS ? streamType : elementaryPid;
+ if (trackIds.get(trackId)) {
+ continue;
+ }
+ trackIds.put(trackId, true);
+
+ TsPayloadReader reader;
+ if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) {
+ reader = id3Reader;
+ } else {
+ reader = payloadReaderFactory.createPayloadReader(streamType, esInfo);
+ if (reader != null) {
+ reader.init(timestampAdjuster, output,
+ new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));
+ }
+ }
+
+ if (reader != null) {
+ tsPayloadReaders.put(elementaryPid, reader);
+ }
+ }
+ if (mode == MODE_HLS) {
+ if (!tracksEnded) {
+ output.endTracks();
+ remainingPmts = 0;
+ tracksEnded = true;
+ }
+ } else {
+ tsPayloadReaders.remove(pid);
+ remainingPmts = mode == MODE_SINGLE_PMT ? 0 : remainingPmts - 1;
+ if (remainingPmts == 0) {
+ output.endTracks();
+ tracksEnded = true;
+ }
+ }
+ }
+
+ /**
+ * Returns the stream info read from the available descriptors. Sets {@code data}'s position to
+ * the end of the descriptors.
+ *
+ * @param data A buffer with its position set to the start of the first descriptor.
+ * @param length The length of descriptors to read from the current position in {@code data}.
+ * @return The stream info read from the available descriptors.
+ */
+ private EsInfo readEsInfo(ParsableByteArray data, int length) {
+ int descriptorsStartPosition = data.getPosition();
+ int descriptorsEndPosition = descriptorsStartPosition + length;
+ int streamType = -1;
+ String language = null;
+ List<DvbSubtitleInfo> dvbSubtitleInfos = null;
+ while (data.getPosition() < descriptorsEndPosition) {
+ int descriptorTag = data.readUnsignedByte();
+ int descriptorLength = data.readUnsignedByte();
+ int positionOfNextDescriptor = data.getPosition() + descriptorLength;
+ if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor
+ long formatIdentifier = data.readUnsignedInt();
+ if (formatIdentifier == AC3_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_AC3;
+ } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_E_AC3;
+ } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) {
+ streamType = TS_STREAM_TYPE_H265;
+ }
+ } else if (descriptorTag == TS_PMT_DESC_AC3) { // AC-3_descriptor in DVB (ETSI EN 300 468)
+ streamType = TS_STREAM_TYPE_AC3;
+ } else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor
+ streamType = TS_STREAM_TYPE_E_AC3;
+ } else if (descriptorTag == TS_PMT_DESC_DTS) { // DTS_descriptor
+ streamType = TS_STREAM_TYPE_DTS;
+ } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) {
+ language = data.readString(3).trim();
+ // Audio type is ignored.
+ } else if (descriptorTag == TS_PMT_DESC_DVBSUBS) {
+ streamType = TS_STREAM_TYPE_DVBSUBS;
+ dvbSubtitleInfos = new ArrayList<>();
+ while (data.getPosition() < positionOfNextDescriptor) {
+ String dvbLanguage = data.readString(3).trim();
+ int dvbSubtitlingType = data.readUnsignedByte();
+ byte[] initializationData = new byte[4];
+ data.readBytes(initializationData, 0, 4);
+ dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType,
+ initializationData));
+ }
+ }
+ // Skip unused bytes of current descriptor.
+ data.skipBytes(positionOfNextDescriptor - data.getPosition());
+ }
+ data.setPosition(descriptorsEndPosition);
+ return new EsInfo(streamType, language, dvbSubtitleInfos,
+ Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition));
+ }
+
+ }
+
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -0,0 +1,202 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses TS packet payload data.
+ */
+public interface TsPayloadReader {
+
+ /**
+ * Factory of {@link TsPayloadReader} instances.
+ */
+ interface Factory {
+
+ /**
+ * Returns the initial mapping from PIDs to payload readers.
+ * <p>
+ * This method allows the injection of payload readers for reserved PIDs, excluding PID 0.
+ *
+ * @return A {@link SparseArray} that maps PIDs to payload readers.
+ */
+ SparseArray<TsPayloadReader> createInitialPayloadReaders();
+
+ /**
+ * Returns a {@link TsPayloadReader} for a given stream type and elementary stream information.
+ * May return null if the stream type is not supported.
+ *
+ * @param streamType Stream type value as defined in the PMT entry or associated descriptors.
+ * @param esInfo Information associated to the elementary stream provided in the PMT.
+ * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid.
+ * {@code null} if the stream is not supported.
+ */
+ TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo);
+
+ }
+
+ /**
+ * Holds information associated with a PMT entry.
+ */
+ final class EsInfo {
+
+ public final int streamType;
+ public final String language;
+ public final List<DvbSubtitleInfo> dvbSubtitleInfos;
+ public final byte[] descriptorBytes;
+
+ /**
+ * @param streamType The type of the stream as defined by the
+ * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.
+ * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
+ * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.
+ * @param descriptorBytes The descriptor bytes associated to the stream.
+ */
+ public EsInfo(int streamType, String language, List<DvbSubtitleInfo> dvbSubtitleInfos,
+ byte[] descriptorBytes) {
+ this.streamType = streamType;
+ this.language = language;
+ this.dvbSubtitleInfos = dvbSubtitleInfos == null ? Collections.<DvbSubtitleInfo>emptyList()
+ : Collections.unmodifiableList(dvbSubtitleInfos);
+ this.descriptorBytes = descriptorBytes;
+ }
+
+ }
+
+ /**
+ * Holds information about a DVB subtitle, as defined in ETSI EN 300 468 V1.11.1 section 6.2.41.
+ */
+ final class DvbSubtitleInfo {
+
+ public final String language;
+ public final int type;
+ public final byte[] initializationData;
+
+ /**
+ * @param language The ISO 639-2 three character language.
+ * @param type The subtitling type.
+ * @param initializationData The composition and ancillary page ids.
+ */
+ public DvbSubtitleInfo(String language, int type, byte[] initializationData) {
+ this.language = language;
+ this.type = type;
+ this.initializationData = initializationData;
+ }
+
+ }
+
+ /**
+ * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s.
+ */
+ final class TrackIdGenerator {
+
+ private static final int ID_UNSET = Integer.MIN_VALUE;
+
+ private final String formatIdPrefix;
+ private final int firstTrackId;
+ private final int trackIdIncrement;
+ private int trackId;
+ private String formatId;
+
+ public TrackIdGenerator(int firstTrackId, int trackIdIncrement) {
+ this(ID_UNSET, firstTrackId, trackIdIncrement);
+ }
+
+ public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) {
+ this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : "";
+ this.firstTrackId = firstTrackId;
+ this.trackIdIncrement = trackIdIncrement;
+ trackId = ID_UNSET;
+ }
+
+ /**
+ * Generates a new set of track and track format ids. Must be called before {@code get*}
+ * methods.
+ */
+ public void generateNewId() {
+ trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement;
+ formatId = formatIdPrefix + trackId;
+ }
+
+ /**
+ * Returns the last generated track id. Must be called after the first {@link #generateNewId()}
+ * call.
+ *
+ * @return The last generated track id.
+ */
+ public int getTrackId() {
+ maybeThrowUninitializedError();
+ return trackId;
+ }
+
+ /**
+ * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no
+ * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be
+ * called after the first {@link #generateNewId()} call.
+ *
+ * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no
+ * {@code programNumber} was provided, the {@code trackId} alone is used as
+ * format id.
+ */
+ public String getFormatId() {
+ maybeThrowUninitializedError();
+ return formatId;
+ }
+
+ private void maybeThrowUninitializedError() {
+ if (trackId == ID_UNSET) {
+ throw new IllegalStateException("generateNewId() must be called before retrieving ids.");
+ }
+ }
+
+ }
+
+ /**
+ * Initializes the payload reader.
+ *
+ * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+ * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
+ * @param idGenerator A {@link PesReader.TrackIdGenerator} that generates unique track ids for the
+ * {@link TrackOutput}s.
+ */
+ void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
+ TrackIdGenerator idGenerator);
+
+ /**
+ * Notifies the reader that a seek has occurred.
+ * <p>
+ * Following a call to this method, the data passed to the next invocation of
+ * {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was
+ * previously passed. Hence the reader should reset any internal state.
+ */
+ void seek();
+
+ /**
+ * Consumes the payload of a TS packet.
+ *
+ * @param data The TS packet. The position will be set to the start of the payload.
+ * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
+ */
+ void consume(ParsableByteArray data, boolean payloadUnitStartIndicator);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
@@ -0,0 +1,132 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.wav;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+
+/** {@link Extractor} to extract samples from a WAV byte stream. */
+public final class WavExtractor implements Extractor, SeekMap {
+
+ /**
+ * Factory for {@link WavExtractor} instances.
+ */
+ public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+ @Override
+ public Extractor[] createExtractors() {
+ return new Extractor[] {new WavExtractor()};
+ }
+
+ };
+
+ /** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
+ private static final int MAX_INPUT_SIZE = 32 * 1024;
+
+ private ExtractorOutput extractorOutput;
+ private TrackOutput trackOutput;
+ private WavHeader wavHeader;
+ private int bytesPerFrame;
+ private int pendingBytes;
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ return WavHeaderReader.peek(input) != null;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ extractorOutput = output;
+ trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ wavHeader = null;
+ output.endTracks();
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ pendingBytes = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (wavHeader == null) {
+ wavHeader = WavHeaderReader.peek(input);
+ if (wavHeader == null) {
+ // Should only happen if the media wasn't sniffed.
+ throw new ParserException("Unsupported or unrecognized wav header.");
+ }
+ Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null,
+ wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(),
+ wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null);
+ trackOutput.format(format);
+ bytesPerFrame = wavHeader.getBytesPerFrame();
+ }
+
+ if (!wavHeader.hasDataBounds()) {
+ WavHeaderReader.skipToData(input, wavHeader);
+ extractorOutput.seekMap(this);
+ }
+
+ int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true);
+ if (bytesAppended != RESULT_END_OF_INPUT) {
+ pendingBytes += bytesAppended;
+ }
+
+ // Samples must consist of a whole number of frames.
+ int pendingFrames = pendingBytes / bytesPerFrame;
+ if (pendingFrames > 0) {
+ long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes);
+ int size = pendingFrames * bytesPerFrame;
+ pendingBytes -= size;
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null);
+ }
+
+ return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ }
+
+ // SeekMap implementation.
+
+ @Override
+ public long getDurationUs() {
+ return wavHeader.getDurationUs();
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public long getPosition(long timeUs) {
+ return wavHeader.getPosition(timeUs);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/wav/WavHeader.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 com.google.android.exoplayer2.extractor.wav;
+
+import com.google.android.exoplayer2.C;
+
+/** Header for a WAV file. */
+/*package*/ final class WavHeader {
+
+ /** Number of audio chanels. */
+ private final int numChannels;
+ /** Sample rate in Hertz. */
+ private final int sampleRateHz;
+ /** Average bytes per second for the sample data. */
+ private final int averageBytesPerSecond;
+ /** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */
+ private final int blockAlignment;
+ /** Bits per sample for the audio data. */
+ private final int bitsPerSample;
+ /** The PCM encoding */
+ @C.PcmEncoding
+ private final int encoding;
+
+ /** Offset to the start of sample data. */
+ private long dataStartPosition;
+ /** Total size in bytes of the sample data. */
+ private long dataSize;
+
+ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment,
+ int bitsPerSample, @C.PcmEncoding int encoding) {
+ this.numChannels = numChannels;
+ this.sampleRateHz = sampleRateHz;
+ this.averageBytesPerSecond = averageBytesPerSecond;
+ this.blockAlignment = blockAlignment;
+ this.bitsPerSample = bitsPerSample;
+ this.encoding = encoding;
+ }
+
+ /** Returns the duration in microseconds of this WAV. */
+ public long getDurationUs() {
+ long numFrames = dataSize / blockAlignment;
+ return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
+ }
+
+ /** Returns the bytes per frame of this WAV. */
+ public int getBytesPerFrame() {
+ return blockAlignment;
+ }
+
+ /** Returns the bitrate of this WAV. */
+ public int getBitrate() {
+ return sampleRateHz * bitsPerSample * numChannels;
+ }
+
+ /** Returns the sample rate in Hertz of this WAV. */
+ public int getSampleRateHz() {
+ return sampleRateHz;
+ }
+
+ /** Returns the number of audio channels in this WAV. */
+ public int getNumChannels() {
+ return numChannels;
+ }
+
+ /** Returns the position in bytes in this WAV for the given time in microseconds. */
+ public long getPosition(long timeUs) {
+ long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
+ // Round down to nearest frame.
+ long position = (unroundedPosition / blockAlignment) * blockAlignment;
+ return Math.min(position, dataSize - blockAlignment) + dataStartPosition;
+ }
+
+ /** Returns the time in microseconds for the given position in bytes in this WAV. */
+ public long getTimeUs(long position) {
+ return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
+ }
+
+ /** Returns true if the data start position and size have been set. */
+ public boolean hasDataBounds() {
+ return dataStartPosition != 0 && dataSize != 0;
+ }
+
+ /** Sets the start position and size in bytes of sample data in this WAV. */
+ public void setDataBounds(long dataStartPosition, long dataSize) {
+ this.dataStartPosition = dataStartPosition;
+ this.dataSize = dataSize;
+ }
+
+ /** Returns the PCM encoding. **/
+ @C.PcmEncoding
+ public int getEncoding() {
+ return encoding;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
@@ -0,0 +1,187 @@
+/*
+ * 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 com.google.android.exoplayer2.extractor.wav;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
+/*package*/ final class WavHeaderReader {
+
+ private static final String TAG = "WavHeaderReader";
+
+ /** Integer PCM audio data. */
+ private static final int TYPE_PCM = 0x0001;
+ /** Extended WAVE format. */
+ private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+
+ /**
+ * Peeks and returns a {@code WavHeader}.
+ *
+ * @param input Input stream to peek the WAV header from.
+ * @throws ParserException If the input file is an incorrect RIFF WAV.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a
+ * supported WAV format.
+ */
+ public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkNotNull(input);
+
+ // Allocate a scratch buffer large enough to store the format chunk.
+ ParsableByteArray scratch = new ParsableByteArray(16);
+
+ // Attempt to read the RIFF chunk.
+ ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
+ if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) {
+ return null;
+ }
+
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int riffFormat = scratch.readInt();
+ if (riffFormat != Util.getIntegerCodeForString("WAVE")) {
+ Log.e(TAG, "Unsupported RIFF format: " + riffFormat);
+ return null;
+ }
+
+ // Skip chunks until we find the format chunk.
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) {
+ input.advancePeekPosition((int) chunkHeader.size);
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ }
+
+ Assertions.checkState(chunkHeader.size >= 16);
+ input.peekFully(scratch.data, 0, 16);
+ scratch.setPosition(0);
+ int type = scratch.readLittleEndianUnsignedShort();
+ int numChannels = scratch.readLittleEndianUnsignedShort();
+ int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt();
+ int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
+ int blockAlignment = scratch.readLittleEndianUnsignedShort();
+ int bitsPerSample = scratch.readLittleEndianUnsignedShort();
+
+ int expectedBlockAlignment = numChannels * bitsPerSample / 8;
+ if (blockAlignment != expectedBlockAlignment) {
+ throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: "
+ + blockAlignment);
+ }
+
+ @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample);
+ if (encoding == C.ENCODING_INVALID) {
+ Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample);
+ return null;
+ }
+
+ if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) {
+ Log.e(TAG, "Unsupported WAV format type: " + type);
+ return null;
+ }
+
+ // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ...
+ input.advancePeekPosition((int) chunkHeader.size - 16);
+
+ return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment,
+ bitsPerSample, encoding);
+ }
+
+ /**
+ * Skips to the data in the given WAV input stream and returns its data size. After calling, the
+ * input stream's position will point to the start of sample data in the WAV.
+ * <p>
+ * If an exception is thrown, the input position will be left pointing to a chunk header.
+ *
+ * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to
+ * a valid chunk header.
+ * @param wavHeader WAV header to populate with data bounds.
+ * @throws ParserException If an error occurs parsing chunks.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If interrupted while reading from input.
+ */
+ public static void skipToData(ExtractorInput input, WavHeader wavHeader)
+ throws IOException, InterruptedException {
+ Assertions.checkNotNull(input);
+ Assertions.checkNotNull(wavHeader);
+
+ // Make sure the peek position is set to the read position before we peek the first header.
+ input.resetPeekPosition();
+
+ ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
+ // Skip all chunks until we hit the data header.
+ ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
+ while (chunkHeader.id != Util.getIntegerCodeForString("data")) {
+ Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id);
+ long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;
+ // Override size of RIFF chunk, since it describes its size as the entire file.
+ if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) {
+ bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4;
+ }
+ if (bytesToSkip > Integer.MAX_VALUE) {
+ throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id);
+ }
+ input.skipFully((int) bytesToSkip);
+ chunkHeader = ChunkHeader.peek(input, scratch);
+ }
+ // Skip past the "data" header.
+ input.skipFully(ChunkHeader.SIZE_IN_BYTES);
+
+ wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
+ }
+
+ /** Container for a WAV chunk header. */
+ private static final class ChunkHeader {
+
+ /** Size in bytes of a WAV chunk header. */
+ public static final int SIZE_IN_BYTES = 8;
+
+ /** 4-character identifier, stored as an integer, for this chunk. */
+ public final int id;
+ /** Size of this chunk in bytes. */
+ public final long size;
+
+ private ChunkHeader(int id, long size) {
+ this.id = id;
+ this.size = size;
+ }
+
+ /**
+ * Peeks and returns a {@link ChunkHeader}.
+ *
+ * @param input Input stream to peek the chunk header from.
+ * @param scratch Buffer for temporary use.
+ * @throws IOException If peeking from the input fails.
+ * @throws InterruptedException If interrupted while peeking from input.
+ * @return A new {@code ChunkHeader} peeked from {@code input}.
+ */
+ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch)
+ throws IOException, InterruptedException {
+ input.peekFully(scratch.data, 0, SIZE_IN_BYTES);
+ scratch.setPosition(0);
+
+ int id = scratch.readInt();
+ long size = scratch.readLittleEndianUnsignedInt();
+
+ return new ChunkHeader(id, size);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -0,0 +1,299 @@
+/*
+ * 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 com.google.android.exoplayer2.mediacodec;
+
+import android.annotation.TargetApi;
+import android.graphics.Point;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo.AudioCapabilities;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Information about a {@link MediaCodec} for a given mime type.
+ */
+@TargetApi(16)
+public final class MediaCodecInfo {
+
+ public static final String TAG = "MediaCodecInfo";
+
+ /**
+ * The name of the decoder.
+ * <p>
+ * May be passed to {@link MediaCodec#createByCodecName(String)} to create an instance of the
+ * decoder.
+ */
+ public final String name;
+
+ /**
+ * Whether the decoder supports seamless resolution switches.
+ *
+ * @see CodecCapabilities#isFeatureSupported(String)
+ * @see CodecCapabilities#FEATURE_AdaptivePlayback
+ */
+ public final boolean adaptive;
+
+ /**
+ * Whether the decoder supports tunneling.
+ *
+ * @see CodecCapabilities#isFeatureSupported(String)
+ * @see CodecCapabilities#FEATURE_TunneledPlayback
+ */
+ public final boolean tunneling;
+
+ private final String mimeType;
+ private final CodecCapabilities capabilities;
+
+ /**
+ * Creates an instance representing an audio passthrough decoder.
+ *
+ * @param name The name of the {@link MediaCodec}.
+ * @return The created instance.
+ */
+ public static MediaCodecInfo newPassthroughInstance(String name) {
+ return new MediaCodecInfo(name, null, null);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param name The name of the {@link MediaCodec}.
+ * @param mimeType A mime type supported by the {@link MediaCodec}.
+ * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
+ * @return The created instance.
+ */
+ public static MediaCodecInfo newInstance(String name, String mimeType,
+ CodecCapabilities capabilities) {
+ return new MediaCodecInfo(name, mimeType, capabilities);
+ }
+
+ /**
+ * @param name The name of the decoder.
+ * @param capabilities The capabilities of the decoder.
+ */
+ private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) {
+ this.name = Assertions.checkNotNull(name);
+ this.mimeType = mimeType;
+ this.capabilities = capabilities;
+ adaptive = capabilities != null && isAdaptive(capabilities);
+ tunneling = capabilities != null && isTunneling(capabilities);
+ }
+
+ /**
+ * The profile levels supported by the decoder.
+ *
+ * @return The profile levels supported by the decoder.
+ */
+ public CodecProfileLevel[] getProfileLevels() {
+ return capabilities == null || capabilities.profileLevels == null ? new CodecProfileLevel[0]
+ : capabilities.profileLevels;
+ }
+
+ /**
+ * Whether the decoder supports the given {@code codec}. If there is insufficient information to
+ * decide, returns true.
+ *
+ * @param codec Codec string as defined in RFC 6381.
+ * @return True if the given codec is supported by the decoder.
+ */
+ public boolean isCodecSupported(String codec) {
+ if (codec == null || mimeType == null) {
+ return true;
+ }
+ String codecMimeType = MimeTypes.getMediaMimeType(codec);
+ if (codecMimeType == null) {
+ return true;
+ }
+ if (!mimeType.equals(codecMimeType)) {
+ logNoSupport("codec.mime " + codec + ", " + codecMimeType);
+ return false;
+ }
+ Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec);
+ if (codecProfileAndLevel == null) {
+ // If we don't know any better, we assume that the profile and level are supported.
+ return true;
+ }
+ for (CodecProfileLevel capabilities : getProfileLevels()) {
+ if (capabilities.profile == codecProfileAndLevel.first
+ && capabilities.level >= codecProfileAndLevel.second) {
+ return true;
+ }
+ }
+ logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType);
+ return false;
+ }
+
+ /**
+ * Whether the decoder supports video with a given width, height and frame rate.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param width Width in pixels.
+ * @param height Height in pixels.
+ * @param frameRate Optional frame rate in frames per second. Ignored if set to
+ * {@link Format#NO_VALUE} or any value less than or equal to 0.
+ * @return Whether the decoder supports video with the given width, height and frame rate.
+ */
+ @TargetApi(21)
+ public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) {
+ if (capabilities == null) {
+ logNoSupport("sizeAndRate.caps");
+ return false;
+ }
+ VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
+ if (videoCapabilities == null) {
+ logNoSupport("sizeAndRate.vCaps");
+ return false;
+ }
+ if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) {
+ // Capabilities are known to be inaccurately reported for vertical resolutions on some devices
+ // (b/31387661). If the video is vertical and the capabilities indicate support if the width
+ // and height are swapped, we assume that the vertical resolution is also supported.
+ if (width >= height
+ || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) {
+ logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate);
+ return false;
+ }
+ logAssumedSupport("sizeAndRate.rotated, " + width + "x" + height + "x" + frameRate);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the smallest video size greater than or equal to a specified size that also satisfies
+ * the {@link MediaCodec}'s width and height alignment requirements.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param width Width in pixels.
+ * @param height Height in pixels.
+ * @return The smallest video size greater than or equal to the specified size that also satisfies
+ * the {@link MediaCodec}'s width and height alignment requirements, or null if not a video
+ * codec.
+ */
+ @TargetApi(21)
+ public Point alignVideoSizeV21(int width, int height) {
+ if (capabilities == null) {
+ logNoSupport("align.caps");
+ return null;
+ }
+ VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
+ if (videoCapabilities == null) {
+ logNoSupport("align.vCaps");
+ return null;
+ }
+ int widthAlignment = videoCapabilities.getWidthAlignment();
+ int heightAlignment = videoCapabilities.getHeightAlignment();
+ return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment,
+ Util.ceilDivide(height, heightAlignment) * heightAlignment);
+ }
+
+ /**
+ * Whether the decoder supports audio with a given sample rate.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param sampleRate The sample rate in Hz.
+ * @return Whether the decoder supports audio with the given sample rate.
+ */
+ @TargetApi(21)
+ public boolean isAudioSampleRateSupportedV21(int sampleRate) {
+ if (capabilities == null) {
+ logNoSupport("sampleRate.caps");
+ return false;
+ }
+ AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();
+ if (audioCapabilities == null) {
+ logNoSupport("sampleRate.aCaps");
+ return false;
+ }
+ if (!audioCapabilities.isSampleRateSupported(sampleRate)) {
+ logNoSupport("sampleRate.support, " + sampleRate);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Whether the decoder supports audio with a given channel count.
+ * <p>
+ * Must not be called if the device SDK version is less than 21.
+ *
+ * @param channelCount The channel count.
+ * @return Whether the decoder supports audio with the given channel count.
+ */
+ @TargetApi(21)
+ public boolean isAudioChannelCountSupportedV21(int channelCount) {
+ if (capabilities == null) {
+ logNoSupport("channelCount.caps");
+ return false;
+ }
+ AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities();
+ if (audioCapabilities == null) {
+ logNoSupport("channelCount.aCaps");
+ return false;
+ }
+ if (audioCapabilities.getMaxInputChannelCount() < channelCount) {
+ logNoSupport("channelCount.support, " + channelCount);
+ return false;
+ }
+ return true;
+ }
+
+ private void logNoSupport(String message) {
+ Log.d(TAG, "NoSupport [" + message + "] [" + name + ", " + mimeType + "] ["
+ + Util.DEVICE_DEBUG_INFO + "]");
+ }
+
+ private void logAssumedSupport(String message) {
+ Log.d(TAG, "AssumedSupport [" + message + "] [" + name + ", " + mimeType + "] ["
+ + Util.DEVICE_DEBUG_INFO + "]");
+ }
+
+ private static boolean isAdaptive(CodecCapabilities capabilities) {
+ return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities);
+ }
+
+ @TargetApi(19)
+ private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
+ }
+
+ @TargetApi(21)
+ private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width,
+ int height, double frameRate) {
+ return frameRate == Format.NO_VALUE || frameRate <= 0
+ ? capabilities.isSizeSupported(width, height)
+ : capabilities.areSizeAndRateSupported(width, height, frameRate);
+ }
+
+ private static boolean isTunneling(CodecCapabilities capabilities) {
+ return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
+ }
+
+ @TargetApi(21)
+ private static boolean isTunnelingV21(CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -0,0 +1,1205 @@
+/*
+ * 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 com.google.android.exoplayer2.mediacodec;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCodec.CodecException;
+import android.media.MediaCodec.CryptoException;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.
+ */
+@TargetApi(16)
+public abstract class MediaCodecRenderer extends BaseRenderer {
+
+ /**
+ * Thrown when a failure occurs instantiating a decoder.
+ */
+ public static class DecoderInitializationException extends Exception {
+
+ private static final int CUSTOM_ERROR_CODE_BASE = -50000;
+ private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;
+ private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;
+
+ /**
+ * The mime type for which a decoder was being initialized.
+ */
+ public final String mimeType;
+
+ /**
+ * Whether it was required that the decoder support a secure output path.
+ */
+ public final boolean secureDecoderRequired;
+
+ /**
+ * The name of the decoder that failed to initialize. Null if no suitable decoder was found.
+ */
+ public final String decoderName;
+
+ /**
+ * An optional developer-readable diagnostic information string. May be null.
+ */
+ public final String diagnosticInfo;
+
+ public DecoderInitializationException(Format format, Throwable cause,
+ boolean secureDecoderRequired, int errorCode) {
+ super("Decoder init failed: [" + errorCode + "], " + format, cause);
+ this.mimeType = format.sampleMimeType;
+ this.secureDecoderRequired = secureDecoderRequired;
+ this.decoderName = null;
+ this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
+ }
+
+ public DecoderInitializationException(Format format, Throwable cause,
+ boolean secureDecoderRequired, String decoderName) {
+ super("Decoder init failed: " + decoderName + ", " + format, cause);
+ this.mimeType = format.sampleMimeType;
+ this.secureDecoderRequired = secureDecoderRequired;
+ this.decoderName = decoderName;
+ this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;
+ }
+
+ @TargetApi(21)
+ private static String getDiagnosticInfoV21(Throwable cause) {
+ if (cause instanceof CodecException) {
+ return ((CodecException) cause).getDiagnosticInfo();
+ }
+ return null;
+ }
+
+ private static String buildCustomDiagnosticInfo(int errorCode) {
+ String sign = errorCode < 0 ? "neg_" : "";
+ return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode);
+ }
+
+ }
+
+ private static final String TAG = "MediaCodecRenderer";
+
+ /**
+ * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
+ * time during which {@link #isReady()} will report true regardless of whether the new codec has
+ * output frames that are ready to be rendered.
+ * <p>
+ * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
+ * other renderers, provided the new codec is able to decode some frames within this time period.
+ */
+ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
+
+ /**
+ * There is no pending adaptive reconfiguration work.
+ */
+ private static final int RECONFIGURATION_STATE_NONE = 0;
+ /**
+ * Codec configuration data needs to be written into the next buffer.
+ */
+ private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
+ /**
+ * Codec configuration data has been written into the next buffer, but that buffer still needs to
+ * be returned to the codec.
+ */
+ private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
+
+ /**
+ * The codec does not need to be re-initialized.
+ */
+ private static final int REINITIALIZATION_STATE_NONE = 0;
+ /**
+ * The input format has changed in a way that requires the codec to be re-initialized, but we
+ * haven't yet signaled an end of stream to the existing codec. We need to do so in order to
+ * ensure that it outputs any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
+ /**
+ * The input format has changed in a way that requires the codec to be re-initialized, and we've
+ * signaled an end of stream to the existing codec. We're waiting for the codec to output an end
+ * of stream signal to indicate that it has output any remaining buffers before we release it.
+ */
+ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
+
+ /**
+ * H.264/AVC buffer to queue when using the adaptation workaround (see
+ * {@link #codecNeedsAdaptationWorkaround(String)}. Consists of three NAL units with start codes:
+ * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be
+ * queued to force a resolution change when adapting to a new format.
+ */
+ private static final byte[] ADAPTATION_WORKAROUND_BUFFER = Util.getBytesFromHexString(
+ "0000016742C00BDA259000000168CE0F13200000016588840DCE7118A0002FBF1C31C3275D78");
+ private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32;
+
+ private final MediaCodecSelector mediaCodecSelector;
+ private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+ private final boolean playClearSamplesWithoutKeys;
+ private final DecoderInputBuffer buffer;
+ private final DecoderInputBuffer flagsOnlyBuffer;
+ private final FormatHolder formatHolder;
+ private final List<Long> decodeOnlyPresentationTimestamps;
+ private final MediaCodec.BufferInfo outputBufferInfo;
+
+ private Format format;
+ private MediaCodec codec;
+ private DrmSession<FrameworkMediaCrypto> drmSession;
+ private DrmSession<FrameworkMediaCrypto> pendingDrmSession;
+ private boolean codecIsAdaptive;
+ private boolean codecNeedsDiscardToSpsWorkaround;
+ private boolean codecNeedsFlushWorkaround;
+ private boolean codecNeedsAdaptationWorkaround;
+ private boolean codecNeedsEosPropagationWorkaround;
+ private boolean codecNeedsEosFlushWorkaround;
+ private boolean codecNeedsEosOutputExceptionWorkaround;
+ private boolean codecNeedsMonoChannelCountWorkaround;
+ private boolean codecNeedsAdaptationWorkaroundBuffer;
+ private boolean shouldSkipAdaptationWorkaroundOutputBuffer;
+ private ByteBuffer[] inputBuffers;
+ private ByteBuffer[] outputBuffers;
+ private long codecHotswapDeadlineMs;
+ private int inputIndex;
+ private int outputIndex;
+ private boolean shouldSkipOutputBuffer;
+ private boolean codecReconfigured;
+ private int codecReconfigurationState;
+ private int codecReinitializationState;
+ private boolean codecReceivedBuffers;
+ private boolean codecReceivedEos;
+
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+ private boolean waitingForKeys;
+ private boolean waitingForFirstSyncFrame;
+
+ protected DecoderCounters decoderCounters;
+
+ /**
+ * @param trackType The track type that the renderer handles. One of the {@code C.TRACK_TYPE_*}
+ * constants defined in {@link C}.
+ * @param mediaCodecSelector A decoder selector.
+ * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+ * media is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ */
+ public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector,
+ DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys) {
+ super(trackType);
+ Assertions.checkState(Util.SDK_INT >= 16);
+ this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
+ this.drmSessionManager = drmSessionManager;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
+ flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
+ formatHolder = new FormatHolder();
+ decodeOnlyPresentationTimestamps = new ArrayList<>();
+ outputBufferInfo = new MediaCodec.BufferInfo();
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ codecReinitializationState = REINITIALIZATION_STATE_NONE;
+ }
+
+ @Override
+ public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+ return ADAPTIVE_NOT_SEAMLESS;
+ }
+
+ @Override
+ public final int supportsFormat(Format format) throws ExoPlaybackException {
+ try {
+ return supportsFormat(mediaCodecSelector, format);
+ } catch (DecoderQueryException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ /**
+ * Returns the extent to which the renderer is capable of supporting a given format.
+ *
+ * @param mediaCodecSelector The decoder selector.
+ * @param format The format.
+ * @return The extent to which the renderer is capable of supporting the given format. See
+ * {@link #supportsFormat(Format)} for more detail.
+ * @throws DecoderQueryException If there was an error querying decoders.
+ */
+ protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
+ throws DecoderQueryException;
+
+ /**
+ * Returns a {@link MediaCodecInfo} for a given format.
+ *
+ * @param mediaCodecSelector The decoder selector.
+ * @param format The format for which a decoder is required.
+ * @param requiresSecureDecoder Whether a secure decoder is required.
+ * @return A {@link MediaCodecInfo} describing the decoder to instantiate, or null if no
+ * suitable decoder exists.
+ * @throws DecoderQueryException Thrown if there was an error querying decoders.
+ */
+ protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
+ Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
+ return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
+ }
+
+ /**
+ * Configures a newly created {@link MediaCodec}.
+ *
+ * @param codecInfo Information about the {@link MediaCodec} being configured.
+ * @param codec The {@link MediaCodec} to configure.
+ * @param format The format for which the codec is being configured.
+ * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
+ * @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
+ */
+ protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
+ MediaCrypto crypto) throws DecoderQueryException;
+
+ @SuppressWarnings("deprecation")
+ protected final void maybeInitCodec() throws ExoPlaybackException {
+ if (!shouldInitCodec()) {
+ return;
+ }
+
+ drmSession = pendingDrmSession;
+ String mimeType = format.sampleMimeType;
+ MediaCrypto mediaCrypto = null;
+ boolean drmSessionRequiresSecureDecoder = false;
+ if (drmSession != null) {
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ } else if (drmSessionState == DrmSession.STATE_OPENED
+ || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
+ mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto();
+ drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType);
+ } else {
+ // The drm session isn't open yet.
+ return;
+ }
+ }
+
+ MediaCodecInfo decoderInfo = null;
+ try {
+ decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
+ if (decoderInfo == null && drmSessionRequiresSecureDecoder) {
+ // The drm session indicates that a secure decoder is required, but the device does not have
+ // one. Assuming that supportsFormat indicated support for the media being played, we know
+ // that it does not require a secure output path. Most CDM implementations allow playback to
+ // proceed with a non-secure decoder in this case, so we try our luck.
+ decoderInfo = getDecoderInfo(mediaCodecSelector, format, false);
+ if (decoderInfo != null) {
+ Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but "
+ + "no secure decoder available. Trying to proceed with " + decoderInfo.name + ".");
+ }
+ }
+ } catch (DecoderQueryException e) {
+ throwDecoderInitError(new DecoderInitializationException(format, e,
+ drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
+ }
+
+ if (decoderInfo == null) {
+ throwDecoderInitError(new DecoderInitializationException(format, null,
+ drmSessionRequiresSecureDecoder,
+ DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
+ }
+
+ String codecName = decoderInfo.name;
+ codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName);
+ codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
+ codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
+ codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
+ codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
+ codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
+ codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
+ codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
+ try {
+ long codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createCodec:" + codecName);
+ codec = MediaCodec.createByCodecName(codecName);
+ TraceUtil.endSection();
+ TraceUtil.beginSection("configureCodec");
+ configureCodec(decoderInfo, codec, format, mediaCrypto);
+ TraceUtil.endSection();
+ TraceUtil.beginSection("startCodec");
+ codec.start();
+ TraceUtil.endSection();
+ long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ onCodecInitialized(codecName, codecInitializedTimestamp,
+ codecInitializedTimestamp - codecInitializingTimestamp);
+ inputBuffers = codec.getInputBuffers();
+ outputBuffers = codec.getOutputBuffers();
+ } catch (Exception e) {
+ throwDecoderInitError(new DecoderInitializationException(format, e,
+ drmSessionRequiresSecureDecoder, codecName));
+ }
+ codecHotswapDeadlineMs = getState() == STATE_STARTED
+ ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET;
+ inputIndex = C.INDEX_UNSET;
+ outputIndex = C.INDEX_UNSET;
+ waitingForFirstSyncFrame = true;
+ decoderCounters.decoderInitCount++;
+ }
+
+ private void throwDecoderInitError(DecoderInitializationException e)
+ throws ExoPlaybackException {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+
+ protected boolean shouldInitCodec() {
+ return codec == null && format != null;
+ }
+
+ protected final MediaCodec getCodec() {
+ return codec;
+ }
+
+ @Override
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ decoderCounters = new DecoderCounters();
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ if (codec != null) {
+ flushCodec();
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ format = null;
+ try {
+ releaseCodec();
+ } finally {
+ try {
+ if (drmSession != null) {
+ drmSessionManager.releaseSession(drmSession);
+ }
+ } finally {
+ try {
+ if (pendingDrmSession != null && pendingDrmSession != drmSession) {
+ drmSessionManager.releaseSession(pendingDrmSession);
+ }
+ } finally {
+ drmSession = null;
+ pendingDrmSession = null;
+ }
+ }
+ }
+ }
+
+ protected void releaseCodec() {
+ if (codec != null) {
+ codecHotswapDeadlineMs = C.TIME_UNSET;
+ inputIndex = C.INDEX_UNSET;
+ outputIndex = C.INDEX_UNSET;
+ waitingForKeys = false;
+ shouldSkipOutputBuffer = false;
+ decodeOnlyPresentationTimestamps.clear();
+ inputBuffers = null;
+ outputBuffers = null;
+ codecReconfigured = false;
+ codecReceivedBuffers = false;
+ codecIsAdaptive = false;
+ codecNeedsDiscardToSpsWorkaround = false;
+ codecNeedsFlushWorkaround = false;
+ codecNeedsAdaptationWorkaround = false;
+ codecNeedsEosPropagationWorkaround = false;
+ codecNeedsEosFlushWorkaround = false;
+ codecNeedsMonoChannelCountWorkaround = false;
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ shouldSkipAdaptationWorkaroundOutputBuffer = false;
+ codecReceivedEos = false;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ codecReinitializationState = REINITIALIZATION_STATE_NONE;
+ decoderCounters.decoderReleaseCount++;
+ buffer.data = null;
+ try {
+ codec.stop();
+ } finally {
+ try {
+ codec.release();
+ } finally {
+ codec = null;
+ if (drmSession != null && pendingDrmSession != drmSession) {
+ try {
+ drmSessionManager.releaseSession(drmSession);
+ } finally {
+ drmSession = null;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ // Do nothing. Overridden to remove throws clause.
+ }
+
+ @Override
+ protected void onStopped() {
+ // Do nothing. Overridden to remove throws clause.
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (outputStreamEnded) {
+ renderToEndOfStream();
+ return;
+ }
+ if (format == null) {
+ // We don't have a format yet, so try and read one.
+ buffer.clear();
+ int result = readSource(formatHolder, flagsOnlyBuffer, true);
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder.format);
+ } else if (result == C.RESULT_BUFFER_READ) {
+ // End of stream read having not read a format.
+ Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
+ inputStreamEnded = true;
+ processEndOfStream();
+ return;
+ } else {
+ // We still don't have a format and can't make progress without one.
+ return;
+ }
+ }
+ // We have a format.
+ maybeInitCodec();
+ if (codec != null) {
+ TraceUtil.beginSection("drainAndFeed");
+ while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
+ while (feedInputBuffer()) {}
+ TraceUtil.endSection();
+ } else {
+ skipSource(positionUs);
+ // We need to read any format changes despite not having a codec so that drmSession can be
+ // updated, and so that we have the most recent format should the codec be initialized. We may
+ // also reach the end of the stream. Note that readSource will not read a sample into a
+ // flags-only buffer.
+ flagsOnlyBuffer.clear();
+ int result = readSource(formatHolder, flagsOnlyBuffer, false);
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder.format);
+ } else if (result == C.RESULT_BUFFER_READ) {
+ Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
+ inputStreamEnded = true;
+ processEndOfStream();
+ }
+ }
+ decoderCounters.ensureUpdated();
+ }
+
+ protected void flushCodec() throws ExoPlaybackException {
+ codecHotswapDeadlineMs = C.TIME_UNSET;
+ inputIndex = C.INDEX_UNSET;
+ outputIndex = C.INDEX_UNSET;
+ waitingForFirstSyncFrame = true;
+ waitingForKeys = false;
+ shouldSkipOutputBuffer = false;
+ decodeOnlyPresentationTimestamps.clear();
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ shouldSkipAdaptationWorkaroundOutputBuffer = false;
+ if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) {
+ releaseCodec();
+ maybeInitCodec();
+ } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) {
+ // We're already waiting to release and re-initialize the codec. Since we're now flushing,
+ // there's no need to wait any longer.
+ releaseCodec();
+ maybeInitCodec();
+ } else {
+ // We can flush and re-use the existing decoder.
+ codec.flush();
+ codecReceivedBuffers = false;
+ }
+ if (codecReconfigured && format != null) {
+ // Any reconfiguration data that we send shortly before the flush may be discarded. We
+ // avoid this issue by sending reconfiguration data following every flush.
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ }
+
+ /**
+ * @return Whether it may be possible to feed more input data.
+ * @throws ExoPlaybackException If an error occurs feeding the input buffer.
+ */
+ private boolean feedInputBuffer() throws ExoPlaybackException {
+ if (codec == null || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ || inputStreamEnded) {
+ // We need to reinitialize the codec or the input stream has ended.
+ return false;
+ }
+
+ if (inputIndex < 0) {
+ inputIndex = codec.dequeueInputBuffer(0);
+ if (inputIndex < 0) {
+ return false;
+ }
+ buffer.data = inputBuffers[inputIndex];
+ buffer.clear();
+ }
+
+ if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+ // We need to re-initialize the codec. Send an end of stream signal to the existing codec so
+ // that it outputs any remaining buffers before we release it.
+ if (codecNeedsEosPropagationWorkaround) {
+ // Do nothing.
+ } else {
+ codecReceivedEos = true;
+ codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ inputIndex = C.INDEX_UNSET;
+ }
+ codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
+ if (codecNeedsAdaptationWorkaroundBuffer) {
+ codecNeedsAdaptationWorkaroundBuffer = false;
+ buffer.data.put(ADAPTATION_WORKAROUND_BUFFER);
+ codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0);
+ inputIndex = C.INDEX_UNSET;
+ codecReceivedBuffers = true;
+ return true;
+ }
+
+ int result;
+ int adaptiveReconfigurationBytes = 0;
+ if (waitingForKeys) {
+ // We've already read an encrypted sample into buffer, and are waiting for keys.
+ result = C.RESULT_BUFFER_READ;
+ } else {
+ // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied
+ // at the start of the buffer that also contains the first frame in the new format.
+ if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {
+ for (int i = 0; i < format.initializationData.size(); i++) {
+ byte[] data = format.initializationData.get(i);
+ buffer.data.put(data);
+ }
+ codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
+ }
+ adaptiveReconfigurationBytes = buffer.data.position();
+ result = readSource(formatHolder, buffer, false);
+ }
+
+ if (result == C.RESULT_NOTHING_READ) {
+ return false;
+ }
+ if (result == C.RESULT_FORMAT_READ) {
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // We received two formats in a row. Clear the current buffer of any reconfiguration data
+ // associated with the first format.
+ buffer.clear();
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ onInputFormatChanged(formatHolder.format);
+ return true;
+ }
+
+ // We've read a buffer.
+ if (buffer.isEndOfStream()) {
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // We received a new format immediately before the end of the stream. We need to clear
+ // the corresponding reconfiguration data from the current buffer, but re-write it into
+ // a subsequent buffer if there are any (e.g. if the user seeks backwards).
+ buffer.clear();
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ inputStreamEnded = true;
+ if (!codecReceivedBuffers) {
+ processEndOfStream();
+ return false;
+ }
+ try {
+ if (codecNeedsEosPropagationWorkaround) {
+ // Do nothing.
+ } else {
+ codecReceivedEos = true;
+ codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ inputIndex = C.INDEX_UNSET;
+ }
+ } catch (CryptoException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ return false;
+ }
+ if (waitingForFirstSyncFrame && !buffer.isKeyFrame()) {
+ buffer.clear();
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // The buffer we just cleared contained reconfiguration data. We need to re-write this
+ // data into a subsequent buffer (if there is one).
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ return true;
+ }
+ waitingForFirstSyncFrame = false;
+ boolean bufferEncrypted = buffer.isEncrypted();
+ waitingForKeys = shouldWaitForKeys(bufferEncrypted);
+ if (waitingForKeys) {
+ return false;
+ }
+ if (codecNeedsDiscardToSpsWorkaround && !bufferEncrypted) {
+ NalUnitUtil.discardToSps(buffer.data);
+ if (buffer.data.position() == 0) {
+ return true;
+ }
+ codecNeedsDiscardToSpsWorkaround = false;
+ }
+ try {
+ long presentationTimeUs = buffer.timeUs;
+ if (buffer.isDecodeOnly()) {
+ decodeOnlyPresentationTimestamps.add(presentationTimeUs);
+ }
+
+ buffer.flip();
+ onQueueInputBuffer(buffer);
+
+ if (bufferEncrypted) {
+ MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer,
+ adaptiveReconfigurationBytes);
+ codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
+ } else {
+ codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0);
+ }
+ inputIndex = C.INDEX_UNSET;
+ codecReceivedBuffers = true;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ decoderCounters.inputBufferCount++;
+ } catch (CryptoException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ return true;
+ }
+
+ private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(DecoderInputBuffer buffer,
+ int adaptiveReconfigurationBytes) {
+ MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
+ if (adaptiveReconfigurationBytes == 0) {
+ return cryptoInfo;
+ }
+ // There must be at least one sub-sample, although numBytesOfClearData is permitted to be
+ // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
+ // bytes to the clear byte count of the first sub-sample.
+ if (cryptoInfo.numBytesOfClearData == null) {
+ cryptoInfo.numBytesOfClearData = new int[1];
+ }
+ cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
+ return cryptoInfo;
+ }
+
+ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+ if (drmSession == null) {
+ return false;
+ }
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ }
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
+ && (bufferEncrypted || !playClearSamplesWithoutKeys);
+ }
+
+ /**
+ * Called when a {@link MediaCodec} has been created and configured.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param name The name of the codec that was initialized.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the codec in milliseconds.
+ */
+ protected void onCodecInitialized(String name, long initializedTimestampMs,
+ long initializationDurationMs) {
+ // Do nothing.
+ }
+
+ /**
+ * Called when a new format is read from the upstream {@link MediaPeriod}.
+ *
+ * @param newFormat The new format.
+ * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
+ */
+ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ Format oldFormat = format;
+ format = newFormat;
+
+ boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
+ : oldFormat.drmInitData);
+ if (drmInitDataChanged) {
+ if (format.drmInitData != null) {
+ if (drmSessionManager == null) {
+ throw ExoPlaybackException.createForRenderer(
+ new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+ }
+ pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
+ if (pendingDrmSession == drmSession) {
+ drmSessionManager.releaseSession(pendingDrmSession);
+ }
+ } else {
+ pendingDrmSession = null;
+ }
+ }
+
+ if (pendingDrmSession == drmSession && codec != null
+ && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
+ codecReconfigured = true;
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround
+ && format.width == oldFormat.width && format.height == oldFormat.height;
+ } else {
+ if (codecReceivedBuffers) {
+ // Signal end of stream and wait for any final output buffers before re-initialization.
+ codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ // There aren't any final output buffers, so perform re-initialization immediately.
+ releaseCodec();
+ maybeInitCodec();
+ }
+ }
+ }
+
+ /**
+ * Called when the output format of the {@link MediaCodec} changes.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param codec The {@link MediaCodec} instance.
+ * @param outputFormat The new output format.
+ * @throws ExoPlaybackException Thrown if an error occurs handling the new output format.
+ */
+ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
+ throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Called immediately before an input buffer is queued into the codec.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param buffer The buffer to be queued.
+ */
+ protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
+ // Do nothing.
+ }
+
+ /**
+ * Called when an output buffer is successfully processed.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @param presentationTimeUs The timestamp associated with the output buffer.
+ */
+ protected void onProcessedOutputBuffer(long presentationTimeUs) {
+ // Do nothing.
+ }
+
+ /**
+ * Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by
+ * sending codec specific initialization data at the start of the next input buffer. If true is
+ * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
+ * returned then the instance will be released, and a new instance will be created for the new
+ * format.
+ * <p>
+ * The default implementation returns false.
+ *
+ * @param codec The existing {@link MediaCodec} instance.
+ * @param codecIsAdaptive Whether the codec is adaptive.
+ * @param oldFormat The format for which the existing instance is configured.
+ * @param newFormat The new format.
+ * @return Whether the existing instance can be reconfigured.
+ */
+ protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, Format oldFormat,
+ Format newFormat) {
+ return false;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ return format != null && !waitingForKeys && (isSourceReady() || outputIndex >= 0
+ || (codecHotswapDeadlineMs != C.TIME_UNSET
+ && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs));
+ }
+
+ /**
+ * Returns the maximum time to block whilst waiting for a decoded output buffer.
+ *
+ * @return The maximum time to block, in microseconds.
+ */
+ protected long getDequeueOutputBufferTimeoutUs() {
+ return 0;
+ }
+
+ /**
+ * @return Whether it may be possible to drain more output data.
+ * @throws ExoPlaybackException If an error occurs draining the output buffer.
+ */
+ @SuppressWarnings("deprecation")
+ private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
+ throws ExoPlaybackException {
+ if (outputIndex < 0) {
+ if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
+ try {
+ outputIndex = codec.dequeueOutputBuffer(outputBufferInfo,
+ getDequeueOutputBufferTimeoutUs());
+ } catch (IllegalStateException e) {
+ processEndOfStream();
+ if (outputStreamEnded) {
+ // Release the codec, as it's in an error state.
+ releaseCodec();
+ }
+ return false;
+ }
+ } else {
+ outputIndex = codec.dequeueOutputBuffer(outputBufferInfo,
+ getDequeueOutputBufferTimeoutUs());
+ }
+ if (outputIndex >= 0) {
+ // We've dequeued a buffer.
+ if (shouldSkipAdaptationWorkaroundOutputBuffer) {
+ shouldSkipAdaptationWorkaroundOutputBuffer = false;
+ codec.releaseOutputBuffer(outputIndex, false);
+ outputIndex = C.INDEX_UNSET;
+ return true;
+ }
+ if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ // The dequeued buffer indicates the end of the stream. Process it immediately.
+ processEndOfStream();
+ outputIndex = C.INDEX_UNSET;
+ return false;
+ } else {
+ // The dequeued buffer is a media buffer. Do some initial setup. The buffer will be
+ // processed by calling processOutputBuffer (possibly multiple times) below.
+ ByteBuffer outputBuffer = outputBuffers[outputIndex];
+ if (outputBuffer != null) {
+ outputBuffer.position(outputBufferInfo.offset);
+ outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
+ }
+ shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs);
+ }
+ } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) {
+ processOutputFormat();
+ return true;
+ } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) {
+ processOutputBuffersChanged();
+ return true;
+ } else /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ {
+ if (codecNeedsEosPropagationWorkaround && (inputStreamEnded
+ || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) {
+ processEndOfStream();
+ }
+ return false;
+ }
+ }
+
+ boolean processedOutputBuffer;
+ if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
+ try {
+ processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec,
+ outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags,
+ outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer);
+ } catch (IllegalStateException e) {
+ processEndOfStream();
+ if (outputStreamEnded) {
+ // Release the codec, as it's in an error state.
+ releaseCodec();
+ }
+ return false;
+ }
+ } else {
+ processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec,
+ outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags,
+ outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer);
+ }
+
+ if (processedOutputBuffer) {
+ onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
+ outputIndex = C.INDEX_UNSET;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Processes a new output format.
+ */
+ private void processOutputFormat() throws ExoPlaybackException {
+ MediaFormat format = codec.getOutputFormat();
+ if (codecNeedsAdaptationWorkaround
+ && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT
+ && format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) {
+ // We assume this format changed event was caused by the adaptation workaround.
+ shouldSkipAdaptationWorkaroundOutputBuffer = true;
+ return;
+ }
+ if (codecNeedsMonoChannelCountWorkaround) {
+ format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
+ }
+ onOutputFormatChanged(codec, format);
+ }
+
+ /**
+ * Processes a change in the output buffers.
+ */
+ @SuppressWarnings("deprecation")
+ private void processOutputBuffersChanged() {
+ outputBuffers = codec.getOutputBuffers();
+ }
+
+ /**
+ * Processes an output media buffer.
+ * <p>
+ * When a new {@link ByteBuffer} is passed to this method its position and limit delineate the
+ * data to be processed. The return value indicates whether the buffer was processed in full. If
+ * true is returned then the next call to this method will receive a new buffer to be processed.
+ * If false is returned then the same buffer will be passed to the next call. An implementation of
+ * this method is free to modify the buffer and can assume that the buffer will not be externally
+ * modified between successive calls. Hence an implementation can, for example, modify the
+ * buffer's position to keep track of how much of the data it has processed.
+ * <p>
+ * Note that the first call to this method following a call to
+ * {@link #onPositionReset(long, boolean)} will always receive a new {@link ByteBuffer} to be
+ * processed.
+ *
+ * @param positionUs The current media time in microseconds, measured at the start of the
+ * current iteration of the rendering loop.
+ * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
+ * measured at the start of the current iteration of the rendering loop.
+ * @param codec The {@link MediaCodec} instance.
+ * @param buffer The output buffer to process.
+ * @param bufferIndex The index of the output buffer.
+ * @param bufferFlags The flags attached to the output buffer.
+ * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds.
+ * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered).
+ *
+ * @return Whether the output buffer was fully processed (e.g. rendered or skipped).
+ * @throws ExoPlaybackException If an error occurs processing the output buffer.
+ */
+ protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs,
+ MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags,
+ long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException;
+
+ /**
+ * Incrementally renders any remaining output.
+ * <p>
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output.
+ */
+ protected void renderToEndOfStream() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Processes an end of stream signal.
+ *
+ * @throws ExoPlaybackException If an error occurs processing the signal.
+ */
+ private void processEndOfStream() throws ExoPlaybackException {
+ if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+ // We're waiting to re-initialize the codec, and have now processed all final buffers.
+ releaseCodec();
+ maybeInitCodec();
+ } else {
+ outputStreamEnded = true;
+ renderToEndOfStream();
+ }
+ }
+
+ private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
+ // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
+ // box presentationTimeUs, creating a Long object that would need to be garbage collected.
+ int size = decodeOnlyPresentationTimestamps.size();
+ for (int i = 0; i < size; i++) {
+ if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) {
+ decodeOnlyPresentationTimestamps.remove(i);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the decoder is known to fail when flushed.
+ * <p>
+ * If true is returned, the renderer will work around the issue by releasing the decoder and
+ * instantiating a new one rather than flushing the current instance.
+ * <p>
+ * See [Internal: b/8347958, b/8543366].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder is known to fail when flushed.
+ */
+ private static boolean codecNeedsFlushWorkaround(String name) {
+ return Util.SDK_INT < 18
+ || (Util.SDK_INT == 18
+ && ("OMX.SEC.avc.dec".equals(name) || "OMX.SEC.avc.dec.secure".equals(name)))
+ || (Util.SDK_INT == 19 && Util.MODEL.startsWith("SM-G800")
+ && ("OMX.Exynos.avc.dec".equals(name) || "OMX.Exynos.avc.dec.secure".equals(name)));
+ }
+
+ /**
+ * Returns whether the decoder is known to get stuck during some adaptations where the resolution
+ * does not change.
+ * <p>
+ * If true is returned, the renderer will work around the issue by queueing and discarding a blank
+ * frame at a different resolution, which resets the codec's internal state.
+ * <p>
+ * See [Internal: b/27807182].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder is known to get stuck during some adaptations.
+ */
+ private static boolean codecNeedsAdaptationWorkaround(String name) {
+ return Util.SDK_INT < 24
+ && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name))
+ && ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE)
+ || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE));
+ }
+
+ /**
+ * Returns whether the decoder is an H.264/AVC decoder known to fail if NAL units are queued
+ * before the codec specific data.
+ * <p>
+ * If true is returned, the renderer will work around the issue by discarding data up to the SPS.
+ *
+ * @param name The name of the decoder.
+ * @param format The format used to configure the decoder.
+ * @return True if the decoder is known to fail if NAL units are queued before CSD.
+ */
+ private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format format) {
+ return Util.SDK_INT < 21 && format.initializationData.isEmpty()
+ && "OMX.MTK.VIDEO.DECODER.AVC".equals(name);
+ }
+
+ /**
+ * Returns whether the decoder is known to handle the propagation of the
+ * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
+ * <p>
+ * If true is returned, the renderer will work around the issue by approximating end of stream
+ * behavior without relying on the flag being propagated through to an output buffer by the
+ * underlying decoder.
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
+ * propagation incorrectly on the host device. False otherwise.
+ */
+ private static boolean codecNeedsEosPropagationWorkaround(String name) {
+ return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name)
+ || "OMX.allwinner.video.decoder.avc".equals(name));
+ }
+
+ /**
+ * Returns whether the decoder is known to behave incorrectly if flushed after receiving an input
+ * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.
+ * <p>
+ * If true is returned, the renderer will work around the issue by instantiating a new decoder
+ * when this case occurs.
+ * <p>
+ * See [Internal: b/8578467, b/23361053].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder is known to behave incorrectly if flushed after receiving an input
+ * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise.
+ */
+ private static boolean codecNeedsEosFlushWorkaround(String name) {
+ return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name))
+ || (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE)
+ && ("OMX.amlogic.avc.decoder.awesome".equals(name)
+ || "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
+ }
+
+ /**
+ * Returns whether the decoder may throw an {@link IllegalStateException} from
+ * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or
+ * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input
+ * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.
+ * <p>
+ * See [Internal: b/17933838].
+ *
+ * @param name The name of the decoder.
+ * @return True if the decoder may throw an exception after receiving an end-of-stream buffer.
+ */
+ private static boolean codecNeedsEosOutputExceptionWorkaround(String name) {
+ return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name);
+ }
+
+ /**
+ * Returns whether the decoder is known to set the number of audio channels in the output format
+ * to 2 for the given input format, whilst only actually outputting a single channel.
+ * <p>
+ * If true is returned then we explicitly override the number of channels in the output format,
+ * setting it to 1.
+ *
+ * @param name The decoder name.
+ * @param format The input format.
+ * @return True if the decoder is known to set the number of audio channels in the output format
+ * to 2 for the given input format, whilst only actually outputting a single channel. False
+ * otherwise.
+ */
+ private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) {
+ return Util.SDK_INT <= 18 && format.channelCount == 1
+ && "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
+ }
+
+ /**
+ * Returns whether the decoder is known to fail when adapting, despite advertising itself as an
+ * adaptive decoder.
+ * <p>
+ * If true is returned then we explicitly disable adaptation for the decoder.
+ *
+ * @param name The decoder name.
+ * @return True if the decoder is known to fail when adapting.
+ */
+ private static boolean codecNeedsDisableAdaptationWorkaround(String name) {
+ return Util.SDK_INT <= 19 && Util.MODEL.equals("ODROID-XU3")
+ && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.google.android.exoplayer2.mediacodec;
+
+import android.media.MediaCodec;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+
+/**
+ * Selector of {@link MediaCodec} instances.
+ */
+public interface MediaCodecSelector {
+
+ /**
+ * Default implementation of {@link MediaCodecSelector}.
+ */
+ MediaCodecSelector DEFAULT = new MediaCodecSelector() {
+
+ @Override
+ public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
+ }
+
+ @Override
+ public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
+ return MediaCodecUtil.getPassthroughDecoderInfo();
+ }
+
+ };
+
+ /**
+ * Selects a decoder to instantiate for a given mime type.
+ *
+ * @param mimeType The mime type for which a decoder is required.
+ * @param requiresSecureDecoder Whether a secure decoder is required.
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
+ * @throws DecoderQueryException Thrown if there was an error querying decoders.
+ */
+ MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+ throws DecoderQueryException;
+
+ /**
+ * Selects a decoder to instantiate for audio passthrough.
+ *
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
+ * exists.
+ * @throws DecoderQueryException Thrown if there was an error querying decoders.
+ */
+ MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -0,0 +1,620 @@
+/*
+ * 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 com.google.android.exoplayer2.mediacodec;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecList;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A utility class for querying the available codecs.
+ */
+@TargetApi(16)
+@SuppressLint("InlinedApi")
+public final class MediaCodecUtil {
+
+ /**
+ * Thrown when an error occurs querying the device for its underlying media capabilities.
+ * <p>
+ * Such failures are not expected in normal operation and are normally temporary (e.g. if the
+ * mediaserver process has crashed and is yet to restart).
+ */
+ public static class DecoderQueryException extends Exception {
+
+ private DecoderQueryException(Throwable cause) {
+ super("Failed to query underlying media codecs", cause);
+ }
+
+ }
+
+ private static final String TAG = "MediaCodecUtil";
+ private static final MediaCodecInfo PASSTHROUGH_DECODER_INFO =
+ MediaCodecInfo.newPassthroughInstance("OMX.google.raw.decoder");
+ private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
+
+ private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>();
+
+ // Codecs to constant mappings.
+ // AVC.
+ private static final SparseIntArray AVC_PROFILE_NUMBER_TO_CONST;
+ private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST;
+ private static final String CODEC_ID_AVC1 = "avc1";
+ private static final String CODEC_ID_AVC2 = "avc2";
+ // HEVC.
+ private static final Map<String, Integer> HEVC_CODEC_STRING_TO_PROFILE_LEVEL;
+ private static final String CODEC_ID_HEV1 = "hev1";
+ private static final String CODEC_ID_HVC1 = "hvc1";
+
+ // Lazily initialized.
+ private static int maxH264DecodableFrameSize = -1;
+
+ private MediaCodecUtil() {}
+
+ /**
+ * Optional call to warm the codec cache for a given mime type.
+ * <p>
+ * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}
+ * and {@link #getDecoderInfos(String, boolean)}.
+ *
+ * @param mimeType The mime type.
+ * @param secure Whether the decoder is required to support secure decryption. Always pass false
+ * unless secure decryption really is required.
+ */
+ public static void warmDecoderInfoCache(String mimeType, boolean secure) {
+ try {
+ getDecoderInfos(mimeType, secure);
+ } catch (DecoderQueryException e) {
+ // Codec warming is best effort, so we can swallow the exception.
+ Log.e(TAG, "Codec warming failed", e);
+ }
+ }
+
+ /**
+ * Returns information about a decoder suitable for audio passthrough.
+ **
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
+ * exists.
+ */
+ public static MediaCodecInfo getPassthroughDecoderInfo() {
+ // TODO: Return null if the raw decoder doesn't exist.
+ return PASSTHROUGH_DECODER_INFO;
+ }
+
+ /**
+ * Returns information about the preferred decoder for a given mime type.
+ *
+ * @param mimeType The mime type.
+ * @param secure Whether the decoder is required to support secure decryption. Always pass false
+ * unless secure decryption really is required.
+ * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
+ * exists.
+ * @throws DecoderQueryException If there was an error querying the available decoders.
+ */
+ public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
+ throws DecoderQueryException {
+ List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
+ return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
+ }
+
+ /**
+ * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by
+ * {@link MediaCodecList}.
+ *
+ * @param mimeType The mime type.
+ * @param secure Whether the decoder is required to support secure decryption. Always pass false
+ * unless secure decryption really is required.
+ * @return A list of all @{link MediaCodecInfo}s for the given mime type, in the order
+ * given by {@link MediaCodecList}.
+ * @throws DecoderQueryException If there was an error querying the available decoders.
+ */
+ public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType,
+ boolean secure) throws DecoderQueryException {
+ CodecKey key = new CodecKey(mimeType, secure);
+ List<MediaCodecInfo> decoderInfos = decoderInfosCache.get(key);
+ if (decoderInfos != null) {
+ return decoderInfos;
+ }
+ MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
+ ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
+ decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
+ if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) {
+ // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the
+ // legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
+ mediaCodecList = new MediaCodecListCompatV16();
+ decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
+ if (!decoderInfos.isEmpty()) {
+ Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ + ". Assuming: " + decoderInfos.get(0).name);
+ }
+ }
+ decoderInfos = Collections.unmodifiableList(decoderInfos);
+ decoderInfosCache.put(key, decoderInfos);
+ return decoderInfos;
+ }
+
+ private static List<MediaCodecInfo> getDecoderInfosInternal(
+ CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
+ try {
+ List<MediaCodecInfo> decoderInfos = new ArrayList<>();
+ String mimeType = key.mimeType;
+ int numberOfCodecs = mediaCodecList.getCodecCount();
+ boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
+ // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+ for (int i = 0; i < numberOfCodecs; i++) {
+ android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
+ String codecName = codecInfo.getName();
+ if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) {
+ for (String supportedType : codecInfo.getSupportedTypes()) {
+ if (supportedType.equalsIgnoreCase(mimeType)) {
+ try {
+ CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType);
+ boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities);
+ if ((secureDecodersExplicit && key.secure == secure)
+ || (!secureDecodersExplicit && !key.secure)) {
+ decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities));
+ } else if (!secureDecodersExplicit && secure) {
+ decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType,
+ capabilities));
+ // It only makes sense to have one synthesized secure decoder, return immediately.
+ return decoderInfos;
+ }
+ } catch (Exception e) {
+ if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) {
+ // Suppress error querying secondary codec capabilities up to API level 23.
+ Log.e(TAG, "Skipping codec " + codecName + " (failed to query capabilities)");
+ } else {
+ // Rethrow error querying primary codec capabilities, or secondary codec
+ // capabilities if API level is greater than 23.
+ Log.e(TAG, "Failed to query codec " + codecName + " (" + supportedType + ")");
+ throw e;
+ }
+ }
+ }
+ }
+ }
+ }
+ return decoderInfos;
+ } catch (Exception e) {
+ // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException
+ // or an IllegalArgumentException here.
+ throw new DecoderQueryException(e);
+ }
+ }
+
+ /**
+ * Returns whether the specified codec is usable for decoding on the current device.
+ */
+ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name,
+ boolean secureDecodersExplicit) {
+ if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
+ return false;
+ }
+
+ // Work around broken audio decoders.
+ if (Util.SDK_INT < 21
+ && ("CIPAACDecoder".equals(name)
+ || "CIPMP3Decoder".equals(name)
+ || "CIPVorbisDecoder".equals(name)
+ || "CIPAMRNBDecoder".equals(name)
+ || "AACDecoder".equals(name)
+ || "MP3Decoder".equals(name))) {
+ return false;
+ }
+
+ // Work around https://github.com/google/ExoPlayer/issues/398
+ if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) {
+ return false;
+ }
+
+ // Work around https://github.com/google/ExoPlayer/issues/1528
+ if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
+ && "a70".equals(Util.DEVICE)) {
+ return false;
+ }
+
+ // Work around an issue where querying/creating a particular MP3 decoder on some devices on
+ // platform API version 16 fails.
+ if (Util.SDK_INT == 16
+ && "OMX.qcom.audio.decoder.mp3".equals(name)
+ && ("dlxu".equals(Util.DEVICE) // HTC Butterfly
+ || "protou".equals(Util.DEVICE) // HTC Desire X
+ || "ville".equals(Util.DEVICE) // HTC One S
+ || "villeplus".equals(Util.DEVICE)
+ || "villec2".equals(Util.DEVICE)
+ || Util.DEVICE.startsWith("gee") // LGE Optimus G
+ || "C6602".equals(Util.DEVICE) // Sony Xperia Z
+ || "C6603".equals(Util.DEVICE)
+ || "C6606".equals(Util.DEVICE)
+ || "C6616".equals(Util.DEVICE)
+ || "L36h".equals(Util.DEVICE)
+ || "SO-02E".equals(Util.DEVICE))) {
+ return false;
+ }
+
+ // Work around an issue where large timestamps are not propagated correctly.
+ if (Util.SDK_INT == 16
+ && "OMX.qcom.audio.decoder.aac".equals(name)
+ && ("C1504".equals(Util.DEVICE) // Sony Xperia E
+ || "C1505".equals(Util.DEVICE)
+ || "C1604".equals(Util.DEVICE) // Sony Xperia E dual
+ || "C1605".equals(Util.DEVICE))) {
+ return false;
+ }
+
+ // Work around https://github.com/google/ExoPlayer/issues/548
+ // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video.
+ if (Util.SDK_INT <= 19
+ && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER)
+ && (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano")
+ || Util.DEVICE.startsWith("jflte") || Util.DEVICE.startsWith("santos")
+ || Util.DEVICE.startsWith("t0"))) {
+ return false;
+ }
+
+ // VP8 decoder on Samsung Galaxy S4 cannot be queried.
+ if (Util.SDK_INT <= 19 && Util.DEVICE.startsWith("jflte")
+ && "OMX.qcom.video.decoder.vp8".equals(name)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the maximum frame size supported by the default H264 decoder.
+ *
+ * @return The maximum frame size for an H264 stream that can be decoded on the device.
+ */
+ public static int maxH264DecodableFrameSize() throws DecoderQueryException {
+ if (maxH264DecodableFrameSize == -1) {
+ int result = 0;
+ MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
+ if (decoderInfo != null) {
+ for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
+ result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
+ }
+ // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
+ // the levels mandated by the Android CDD.
+ result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));
+ }
+ maxH264DecodableFrameSize = result;
+ }
+ return maxH264DecodableFrameSize;
+ }
+
+ /**
+ * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
+ * codec description string (as defined by RFC 6381).
+ *
+ * @param codec A codec description string, as defined by RFC 6381.
+ * @return A pair (profile constant, level constant) if {@code codec} is well-formed and
+ * recognized, or null otherwise
+ */
+ public static Pair<Integer, Integer> getCodecProfileAndLevel(String codec) {
+ if (codec == null) {
+ return null;
+ }
+ String[] parts = codec.split("\\.");
+ switch (parts[0]) {
+ case CODEC_ID_HEV1:
+ case CODEC_ID_HVC1:
+ return getHevcProfileAndLevel(codec, parts);
+ case CODEC_ID_AVC1:
+ case CODEC_ID_AVC2:
+ return getAvcProfileAndLevel(codec, parts);
+ default:
+ return null;
+ }
+ }
+
+ private static Pair<Integer, Integer> getHevcProfileAndLevel(String codec, String[] parts) {
+ if (parts.length < 4) {
+ // The codec has fewer parts than required by the HEVC codec string format.
+ Log.w(TAG, "Ignoring malformed HEVC codec string: " + codec);
+ return null;
+ }
+ // The profile_space gets ignored.
+ Matcher matcher = PROFILE_PATTERN.matcher(parts[1]);
+ if (!matcher.matches()) {
+ Log.w(TAG, "Ignoring malformed HEVC codec string: " + codec);
+ return null;
+ }
+ String profileString = matcher.group(1);
+ int profile;
+ if ("1".equals(profileString)) {
+ profile = CodecProfileLevel.HEVCProfileMain;
+ } else if ("2".equals(profileString)) {
+ profile = CodecProfileLevel.HEVCProfileMain10;
+ } else {
+ Log.w(TAG, "Unknown HEVC profile string: " + profileString);
+ return null;
+ }
+ Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(parts[3]);
+ if (level == null) {
+ Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1));
+ return null;
+ }
+ return new Pair<>(profile, level);
+ }
+
+ private static Pair<Integer, Integer> getAvcProfileAndLevel(String codec, String[] codecsParts) {
+ if (codecsParts.length < 2) {
+ // The codec has fewer parts than required by the AVC codec string format.
+ Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
+ return null;
+ }
+ Integer profileInteger;
+ Integer levelInteger;
+ try {
+ if (codecsParts[1].length() == 6) {
+ // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal.
+ profileInteger = Integer.parseInt(codecsParts[1].substring(0, 2), 16);
+ levelInteger = Integer.parseInt(codecsParts[1].substring(4), 16);
+ } else if (codecsParts.length >= 3) {
+ // Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal.
+ profileInteger = Integer.parseInt(codecsParts[1]);
+ levelInteger = Integer.parseInt(codecsParts[2]);
+ } else {
+ // We don't recognize the format.
+ Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
+ return null;
+ }
+
+ Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger);
+ if (profile == null) {
+ Log.w(TAG, "Unknown AVC profile: " + profileInteger);
+ return null;
+ }
+ Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger);
+ if (level == null) {
+ Log.w(TAG, "Unknown AVC level: " + levelInteger);
+ return null;
+ }
+ return new Pair<>(profile, level);
+ }
+
+ /**
+ * Conversion values taken from ISO 14496-10 Table A-1.
+ *
+ * @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
+ * @return maximum frame size that can be decoded by a decoder with the specified avc level
+ * (or {@code -1} if the level is not recognized)
+ */
+ private static int avcLevelToMaxFrameSize(int avcLevel) {
+ switch (avcLevel) {
+ case CodecProfileLevel.AVCLevel1: return 99 * 16 * 16;
+ case CodecProfileLevel.AVCLevel1b: return 99 * 16 * 16;
+ case CodecProfileLevel.AVCLevel12: return 396 * 16 * 16;
+ case CodecProfileLevel.AVCLevel13: return 396 * 16 * 16;
+ case CodecProfileLevel.AVCLevel2: return 396 * 16 * 16;
+ case CodecProfileLevel.AVCLevel21: return 792 * 16 * 16;
+ case CodecProfileLevel.AVCLevel22: return 1620 * 16 * 16;
+ case CodecProfileLevel.AVCLevel3: return 1620 * 16 * 16;
+ case CodecProfileLevel.AVCLevel31: return 3600 * 16 * 16;
+ case CodecProfileLevel.AVCLevel32: return 5120 * 16 * 16;
+ case CodecProfileLevel.AVCLevel4: return 8192 * 16 * 16;
+ case CodecProfileLevel.AVCLevel41: return 8192 * 16 * 16;
+ case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16;
+ case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16;
+ case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16;
+ default: return -1;
+ }
+ }
+
+ private interface MediaCodecListCompat {
+
+ /**
+ * The number of codecs in the list.
+ */
+ int getCodecCount();
+
+ /**
+ * The info at the specified index in the list.
+ *
+ * @param index The index.
+ */
+ android.media.MediaCodecInfo getCodecInfoAt(int index);
+
+ /**
+ * Returns whether secure decoders are explicitly listed, if present.
+ */
+ boolean secureDecodersExplicit();
+
+ /**
+ * Whether secure playback is supported for the given {@link CodecCapabilities}, which should
+ * have been obtained from a {@link android.media.MediaCodecInfo} obtained from this list.
+ */
+ boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities);
+
+ }
+
+ @TargetApi(21)
+ private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
+
+ private final int codecKind;
+
+ private android.media.MediaCodecInfo[] mediaCodecInfos;
+
+ public MediaCodecListCompatV21(boolean includeSecure) {
+ codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS;
+ }
+
+ @Override
+ public int getCodecCount() {
+ ensureMediaCodecInfosInitialized();
+ return mediaCodecInfos.length;
+ }
+
+ @Override
+ public android.media.MediaCodecInfo getCodecInfoAt(int index) {
+ ensureMediaCodecInfosInitialized();
+ return mediaCodecInfos[index];
+ }
+
+ @Override
+ public boolean secureDecodersExplicit() {
+ return true;
+ }
+
+ @Override
+ public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
+ }
+
+ private void ensureMediaCodecInfosInitialized() {
+ if (mediaCodecInfos == null) {
+ mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();
+ }
+ }
+
+ }
+
+ @SuppressWarnings("deprecation")
+ private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {
+
+ @Override
+ public int getCodecCount() {
+ return MediaCodecList.getCodecCount();
+ }
+
+ @Override
+ public android.media.MediaCodecInfo getCodecInfoAt(int index) {
+ return MediaCodecList.getCodecInfoAt(index);
+ }
+
+ @Override
+ public boolean secureDecodersExplicit() {
+ return false;
+ }
+
+ @Override
+ public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
+ // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure
+ // H264 decoder exists.
+ return MimeTypes.VIDEO_H264.equals(mimeType);
+ }
+
+ }
+
+ private static final class CodecKey {
+
+ public final String mimeType;
+ public final boolean secure;
+
+ public CodecKey(String mimeType, boolean secure) {
+ this.mimeType = mimeType;
+ this.secure = secure;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
+ result = prime * result + (secure ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != CodecKey.class) {
+ return false;
+ }
+ CodecKey other = (CodecKey) obj;
+ return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure;
+ }
+
+ }
+
+ static {
+ AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
+ AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);
+ AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain);
+ AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended);
+ AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh);
+
+ AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray();
+ AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1);
+ // TODO: Find int for CodecProfileLevel.AVCLevel1b.
+ AVC_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AVCLevel11);
+ AVC_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AVCLevel12);
+ AVC_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AVCLevel13);
+ AVC_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AVCLevel2);
+ AVC_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AVCLevel21);
+ AVC_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AVCLevel22);
+ AVC_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.AVCLevel3);
+ AVC_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.AVCLevel31);
+ AVC_LEVEL_NUMBER_TO_CONST.put(32, CodecProfileLevel.AVCLevel32);
+ AVC_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.AVCLevel4);
+ AVC_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.AVCLevel41);
+ AVC_LEVEL_NUMBER_TO_CONST.put(42, CodecProfileLevel.AVCLevel42);
+ AVC_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.AVCLevel5);
+ AVC_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.AVCLevel51);
+ AVC_LEVEL_NUMBER_TO_CONST.put(52, CodecProfileLevel.AVCLevel52);
+
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL = new HashMap<>();
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L30", CodecProfileLevel.HEVCMainTierLevel1);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L60", CodecProfileLevel.HEVCMainTierLevel2);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L63", CodecProfileLevel.HEVCMainTierLevel21);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L90", CodecProfileLevel.HEVCMainTierLevel3);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L93", CodecProfileLevel.HEVCMainTierLevel31);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L120", CodecProfileLevel.HEVCMainTierLevel4);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L123", CodecProfileLevel.HEVCMainTierLevel41);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L150", CodecProfileLevel.HEVCMainTierLevel5);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L153", CodecProfileLevel.HEVCMainTierLevel51);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L156", CodecProfileLevel.HEVCMainTierLevel52);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L180", CodecProfileLevel.HEVCMainTierLevel6);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L183", CodecProfileLevel.HEVCMainTierLevel61);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L186", CodecProfileLevel.HEVCMainTierLevel62);
+
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H30", CodecProfileLevel.HEVCHighTierLevel1);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H60", CodecProfileLevel.HEVCHighTierLevel2);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H63", CodecProfileLevel.HEVCHighTierLevel21);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H90", CodecProfileLevel.HEVCHighTierLevel3);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H93", CodecProfileLevel.HEVCHighTierLevel31);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H120", CodecProfileLevel.HEVCHighTierLevel4);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H123", CodecProfileLevel.HEVCHighTierLevel41);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H150", CodecProfileLevel.HEVCHighTierLevel5);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H153", CodecProfileLevel.HEVCHighTierLevel51);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H156", CodecProfileLevel.HEVCHighTierLevel52);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H180", CodecProfileLevel.HEVCHighTierLevel6);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H183", CodecProfileLevel.HEVCHighTierLevel61);
+ HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H186", CodecProfileLevel.HEVCHighTierLevel62);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/Metadata.java
@@ -0,0 +1,120 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+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 {}
+
+ private final Entry[] entries;
+
+ /**
+ * @param entries The metadata entries.
+ */
+ public Metadata(Entry... entries) {
+ this.entries = entries == null ? new Entry[0] : entries;
+ }
+
+ /**
+ * @param entries The metadata entries.
+ */
+ public Metadata(List<? extends Entry> entries) {
+ if (entries != null) {
+ this.entries = new Entry[entries.size()];
+ entries.toArray(this.entries);
+ } else {
+ this.entries = new Entry[0];
+ }
+ }
+
+ /* 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];
+ }
+
+ @Override
+ public boolean equals(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 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[0];
+ }
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata;
+
+/**
+ * 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.
+ * @throws MetadataDecoderException If a problem occurred decoding the data.
+ */
+ Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/MetadataDecoderException.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata;
+
+/**
+ * Thrown when an error occurs decoding metadata.
+ */
+public class MetadataDecoderException extends Exception {
+
+ /**
+ * @param message The detail message for this exception.
+ */
+ public MetadataDecoderException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message The detail message for this exception.
+ * @param cause The cause of this exception.
+ */
+ public MetadataDecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
+import 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>
+ * <li>EMSG ({@link EventMessageDecoder})</li>
+ * <li>SCTE-35 ({@link SpliceInfoDecoder})</li>
+ * </ul>
+ */
+ MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() {
+
+ @Override
+ public boolean supportsFormat(Format format) {
+ String mimeType = format.sampleMimeType;
+ return MimeTypes.APPLICATION_ID3.equals(mimeType)
+ || MimeTypes.APPLICATION_EMSG.equals(mimeType)
+ || MimeTypes.APPLICATION_SCTE35.equals(mimeType);
+ }
+
+ @Override
+ public MetadataDecoder createDecoder(Format format) {
+ switch (format.sampleMimeType) {
+ case MimeTypes.APPLICATION_ID3:
+ return new Id3Decoder();
+ case MimeTypes.APPLICATION_EMSG:
+ return new EventMessageDecoder();
+ case MimeTypes.APPLICATION_SCTE35:
+ return new SpliceInfoDecoder();
+ default:
+ throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
+ }
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/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 com.google.android.exoplayer2.metadata;
+
+import com.google.android.exoplayer2.Format;
+import 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);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -0,0 +1,200 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+/**
+ * A renderer for metadata.
+ */
+public final class MetadataRenderer extends BaseRenderer implements Callback {
+
+ /**
+ * Receives output from a {@link MetadataRenderer}.
+ */
+ public interface Output {
+
+ /**
+ * Called each time there is a metadata associated with current playback time.
+ *
+ * @param metadata The metadata.
+ */
+ void onMetadata(Metadata metadata);
+
+ }
+
+ 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 Output output;
+ private final Handler outputHandler;
+ private final FormatHolder formatHolder;
+ private final MetadataInputBuffer buffer;
+ private final Metadata[] pendingMetadata;
+ private final long[] pendingMetadataTimestamps;
+
+ private int pendingMetadataIndex;
+ private int pendingMetadataCount;
+ private MetadataDecoder decoder;
+ private boolean inputStreamEnded;
+
+ /**
+ * @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(Output output, 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(Output output, Looper outputLooper,
+ MetadataDecoderFactory decoderFactory) {
+ super(C.TRACK_TYPE_METADATA);
+ this.output = Assertions.checkNotNull(output);
+ this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
+ this.decoderFactory = Assertions.checkNotNull(decoderFactory);
+ formatHolder = new FormatHolder();
+ buffer = new MetadataInputBuffer();
+ pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT];
+ pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT];
+ }
+
+ @Override
+ public int supportsFormat(Format format) {
+ return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
+ }
+
+ @Override
+ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ decoder = decoderFactory.createDecoder(formats[0]);
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) {
+ flushPendingMetadata();
+ inputStreamEnded = false;
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) {
+ buffer.clear();
+ 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 = formatHolder.format.subsampleOffsetUs;
+ buffer.flip();
+ try {
+ int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;
+ pendingMetadata[index] = decoder.decode(buffer);
+ pendingMetadataTimestamps[index] = buffer.timeUs;
+ pendingMetadataCount++;
+ } catch (MetadataDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+ }
+ }
+
+ if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) {
+ invokeRenderer(pendingMetadata[pendingMetadataIndex]);
+ pendingMetadata[pendingMetadataIndex] = null;
+ pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT;
+ pendingMetadataCount--;
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ flushPendingMetadata();
+ decoder = null;
+ super.onDisabled();
+ }
+
+ @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;
+ }
+
+ @SuppressWarnings("unchecked")
+ @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);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -0,0 +1,141 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.emsg;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.metadata.Metadata;
+import 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 {
+
+ /**
+ * 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 = in.readString();
+ value = in.readString();
+ durationMs = in.readLong();
+ id = in.readLong();
+ messageData = in.createByteArray();
+ }
+
+ @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(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);
+ }
+
+ // 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.emsg;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1.
+ * <p>
+ * Atom data should be provided to the decoder without the full atom header (i.e. starting from the
+ * first byte of the scheme_id_uri field).
+ */
+public final class EventMessageDecoder implements MetadataDecoder {
+
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = inputBuffer.data;
+ byte[] data = buffer.array();
+ int size = buffer.limit();
+ ParsableByteArray emsgData = new ParsableByteArray(data, size);
+ String schemeIdUri = emsgData.readNullTerminatedString();
+ String value = emsgData.readNullTerminatedString();
+ long timescale = emsgData.readUnsignedInt();
+ emsgData.skipBytes(4); // presentation_time_delta
+ long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale;
+ long id = emsgData.readUnsignedInt();
+ byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
+ return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
@@ -0,0 +1,97 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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;
+ public final String description;
+ public final int pictureType;
+ public final byte[] pictureData;
+
+ public ApicFrame(String mimeType, 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 = in.readString();
+ description = in.readString();
+ pictureType = in.readInt();
+ pictureData = in.createByteArray();
+ }
+
+ @Override
+ public boolean equals(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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+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(in.readString());
+ data = in.createByteArray();
+ }
+
+ @Override
+ public boolean equals(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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -0,0 +1,142 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import com.google.android.exoplayer2.C;
+import 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 = 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(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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import 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 = in.readString();
+ this.isRoot = in.readByte() != 0;
+ this.isOrdered = in.readByte() != 0;
+ this.children = 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(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 (int i = 0; i < subFrames.length; i++) {
+ dest.writeParcelable(subFrames[i], 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
@@ -0,0 +1,91 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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 = in.readString();
+ description = in.readString();
+ text = in.readString();
+ }
+
+ @Override
+ public boolean equals(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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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 = in.readString();
+ filename = in.readString();
+ description = in.readString();
+ data = in.createByteArray();
+ }
+
+ @Override
+ public boolean equals(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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -0,0 +1,766 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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);
+
+ }
+
+ private static final String TAG = "Id3Decoder";
+
+ /**
+ * The first three bytes of a well formed ID3 tag header.
+ */
+ public static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+ /**
+ * 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;
+
+ private final FramePredicate framePredicate;
+
+ public Id3Decoder() {
+ this(null);
+ }
+
+ /**
+ * @param framePredicate Determines which frames are decoded. May be null to decode all frames.
+ */
+ public Id3Decoder(FramePredicate framePredicate) {
+ this.framePredicate = framePredicate;
+ }
+
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = 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.
+ */
+ 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.
+ */
+ 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: " + 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);
+ }
+ }
+
+ private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
+ boolean unsignedIntFrameSizeHack, int frameHeaderSize, 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 {
+ 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);
+ }
+ }
+
+ 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);
+
+ String value;
+ int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ if (valueStartIndex < data.length) {
+ int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
+ value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
+ } else {
+ value = "";
+ }
+
+ return new TextInformationFrame("TXXX", description, value);
+ }
+
+ 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);
+ }
+
+ 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);
+
+ String url;
+ int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ if (urlStartIndex < data.length) {
+ int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
+ url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1");
+ } else {
+ url = "";
+ }
+
+ 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");
+
+ byte[] privateData;
+ int privateDataStartIndex = ownerEndIndex + 1;
+ if (privateDataStartIndex < data.length) {
+ privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length);
+ } else {
+ privateData = new byte[0];
+ }
+
+ 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 = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex,
+ charset);
+
+ int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
+ int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
+ String description = new String(data, descriptionStartIndex,
+ descriptionEndIndex - descriptionStartIndex, charset);
+
+ int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ byte[] objectData = Arrays.copyOfRange(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 (mimeType.equals("image/jpg")) {
+ 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 = Arrays.copyOfRange(data, pictureDataStartIndex, data.length);
+
+ return new ApicFrame(mimeType, description, pictureType, pictureData);
+ }
+
+ 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);
+
+ String text;
+ int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ if (textStartIndex < data.length) {
+ int textEndIndex = indexOfEos(data, textStartIndex, encoding);
+ text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
+ } else {
+ text = "";
+ }
+
+ return new CommentFrame(language, description, text);
+ }
+
+ private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
+ int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize,
+ 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,
+ 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 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;
+ for (int i = data.getPosition(); i + 1 < length; i++) {
+ if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
+ System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 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_ISO_8859_1:
+ return "ISO-8859-1";
+ 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";
+ 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;
+ }
+
+ 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;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * 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 = Assertions.checkNotNull(id);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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 = in.readString();
+ privateData = in.createByteArray();
+ }
+
+ @Override
+ public boolean equals(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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Text information ID3 frame.
+ */
+public final class TextInformationFrame extends Id3Frame {
+
+ public final String description;
+ public final String value;
+
+ public TextInformationFrame(String id, String description, String value) {
+ super(id);
+ this.description = description;
+ this.value = value;
+ }
+
+ /* package */ TextInformationFrame(Parcel in) {
+ super(in.readString());
+ description = in.readString();
+ value = in.readString();
+ }
+
+ @Override
+ public boolean equals(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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Url link ID3 frame.
+ */
+public final class UrlLinkFrame extends Id3Frame {
+
+ public final String description;
+ public final String url;
+
+ public UrlLinkFrame(String id, String description, String url) {
+ super(id);
+ this.description = description;
+ this.url = url;
+ }
+
+ /* package */ UrlLinkFrame(Parcel in) {
+ super(in.readString());
+ description = in.readString();
+ url = in.readString();
+ }
+
+ @Override
+ public boolean equals(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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Represents a private command as defined in SCTE35, Section 9.3.6.
+ */
+public final class PrivateCommand extends SpliceCommand {
+
+ public final long ptsAdjustment;
+ public final long identifier;
+ 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 = new byte[in.readInt()];
+ in.readByteArray(commandBytes);
+ }
+
+ /* 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.writeInt(commandBytes.length);
+ 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.scte35;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+
+/**
+ * Superclass for SCTE35 splice commands.
+ */
+public abstract class SpliceCommand implements Metadata.Entry {
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.scte35;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataDecoderException;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.nio.ByteBuffer;
+
+/**
+ * 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;
+
+ private TimestampAdjuster timestampAdjuster;
+
+ public SpliceInfoDecoder() {
+ sectionData = new ParsableByteArray();
+ sectionHeader = new ParsableBitArray();
+ }
+
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
+ // Internal timestamps adjustment.
+ if (timestampAdjuster == null
+ || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
+ timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs);
+ timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs);
+ }
+
+ ByteBuffer buffer = inputBuffer.data;
+ 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);
+ 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);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
@@ -0,0 +1,205 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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 {
+
+ public final long spliceEventId;
+ public final boolean spliceEventCancelIndicator;
+ public final boolean outOfNetworkIndicator;
+ public final boolean programSpliceFlag;
+ public final boolean spliceImmediateFlag;
+ public final long programSplicePts;
+ public final long programSplicePlaybackPositionUs;
+ public final List<ComponentSplice> componentSpliceList;
+ public final boolean autoReturn;
+ public final long breakDuration;
+ public final int uniqueProgramId;
+ public final int availNum;
+ 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 breakDuration,
+ 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.breakDuration = breakDuration;
+ 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;
+ breakDuration = 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 duration = 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;
+ duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+ }
+ uniqueProgramId = sectionData.readUnsignedShort();
+ availNum = sectionData.readUnsignedByte();
+ availsExpected = sectionData.readUnsignedByte();
+ }
+ return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+ programSpliceFlag, spliceImmediateFlag, programSplicePts,
+ timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn,
+ duration, 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(breakDuration);
+ 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/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 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
@@ -0,0 +1,226 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import 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 {
+
+ public final long spliceEventId;
+ public final boolean spliceEventCancelIndicator;
+ public final boolean outOfNetworkIndicator;
+ public final boolean programSpliceFlag;
+ public final long utcSpliceTime;
+ public final List<ComponentSplice> componentSpliceList;
+ public final boolean autoReturn;
+ public final long breakDuration;
+ public final int uniqueProgramId;
+ public final int availNum;
+ public final int availsExpected;
+
+ private Event(long spliceEventId, boolean spliceEventCancelIndicator,
+ boolean outOfNetworkIndicator, boolean programSpliceFlag,
+ List<ComponentSplice> componentSpliceList, long utcSpliceTime, boolean autoReturn,
+ long breakDuration, 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.breakDuration = breakDuration;
+ 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.breakDuration = 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 duration = 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;
+ duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+ }
+ uniqueProgramId = sectionData.readUnsignedShort();
+ availNum = sectionData.readUnsignedByte();
+ availsExpected = sectionData.readUnsignedByte();
+ }
+ return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+ programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, duration, 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(breakDuration);
+ 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);
+ }
+
+ }
+
+ 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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 {
+
+ public final long ptsTime;
+ 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];
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
@@ -0,0 +1,315 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Interface for callbacks to be notified of adaptive {@link MediaSource} events.
+ */
+public interface AdaptiveMediaSourceEventListener {
+
+ /**
+ * Called when a load begins.
+ *
+ * @param dataSpec Defines the data being loaded.
+ * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+ * being loaded.
+ * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+ * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+ * @param trackFormat The format of the track to which the data belongs. Null if the data does
+ * not belong to a track.
+ * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+ * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ * @param trackSelectionData Optional data associated with the selection of the track to which the
+ * data belongs. Null if the data does not belong to a track.
+ * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+ * the load is not for media data.
+ * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+ * load is not for media data.
+ * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
+ */
+ void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+ long mediaEndTimeMs, long elapsedRealtimeMs);
+
+ /**
+ * Called when a load ends.
+ *
+ * @param dataSpec Defines the data being loaded.
+ * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+ * being loaded.
+ * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+ * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+ * @param trackFormat The format of the track to which the data belongs. Null if the data does
+ * not belong to a track.
+ * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+ * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ * @param trackSelectionData Optional data associated with the selection of the track to which the
+ * data belongs. Null if the data does not belong to a track.
+ * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+ * the load is not for media data.
+ * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+ * load is not for media data.
+ * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended.
+ * @param loadDurationMs The duration of the load.
+ * @param bytesLoaded The number of bytes that were loaded.
+ */
+ void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+ long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
+
+ /**
+ * Called when a load is canceled.
+ *
+ * @param dataSpec Defines the data being loaded.
+ * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+ * being loaded.
+ * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+ * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+ * @param trackFormat The format of the track to which the data belongs. Null if the data does
+ * not belong to a track.
+ * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+ * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ * @param trackSelectionData Optional data associated with the selection of the track to which the
+ * data belongs. Null if the data does not belong to a track.
+ * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+ * the load is not for media data.
+ * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+ * load is not for media data.
+ * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was
+ * canceled.
+ * @param loadDurationMs The duration of the load up to the point at which it was canceled.
+ * @param bytesLoaded The number of bytes that were loaded prior to cancelation.
+ */
+ void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+ long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
+
+ /**
+ * Called when a load error occurs.
+ * <p>
+ * The error may or may not have resulted in the load being canceled, as indicated by the
+ * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
+ * <em>not</em> be called in addition to this method.
+ *
+ * @param dataSpec Defines the data being loaded.
+ * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+ * being loaded.
+ * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+ * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+ * @param trackFormat The format of the track to which the data belongs. Null if the data does
+ * not belong to a track.
+ * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+ * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ * @param trackSelectionData Optional data associated with the selection of the track to which the
+ * data belongs. Null if the data does not belong to a track.
+ * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+ * the load is not for media data.
+ * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+ * load is not for media data.
+ * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error
+ * occurred.
+ * @param loadDurationMs The duration of the load up to the point at which the error occurred.
+ * @param bytesLoaded The number of bytes that were loaded prior to the error.
+ * @param error The load error.
+ * @param wasCanceled Whether the load was canceled as a result of the error.
+ */
+ void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+ long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
+ IOException error, boolean wasCanceled);
+
+ /**
+ * Called when data is removed from the back of a media buffer, typically so that it can be
+ * re-buffered in a different format.
+ *
+ * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @param mediaStartTimeMs The start time of the media being discarded.
+ * @param mediaEndTimeMs The end time of the media being discarded.
+ */
+ void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs);
+
+ /**
+ * Called when a downstream format change occurs (i.e. when the format of the media being read
+ * from one or more {@link SampleStream}s provided by the source changes).
+ *
+ * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @param trackFormat The format of the track to which the data belongs. Null if the data does
+ * not belong to a track.
+ * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+ * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+ * @param trackSelectionData Optional data associated with the selection of the track to which the
+ * data belongs. Null if the data does not belong to a track.
+ * @param mediaTimeMs The media time at which the change occurred.
+ */
+ void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason,
+ Object trackSelectionData, long mediaTimeMs);
+
+ /**
+ * Dispatches events to a {@link AdaptiveMediaSourceEventListener}.
+ */
+ final class EventDispatcher {
+
+ private final Handler handler;
+ private final AdaptiveMediaSourceEventListener listener;
+ private final long mediaTimeOffsetMs;
+
+ public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) {
+ this(handler, listener, 0);
+ }
+
+ public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener,
+ long mediaTimeOffsetMs) {
+ this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+ this.listener = listener;
+ this.mediaTimeOffsetMs = mediaTimeOffsetMs;
+ }
+
+ public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) {
+ return new EventDispatcher(handler, listener, mediaTimeOffsetMs);
+ }
+
+ public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
+ loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+ null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs);
+ }
+
+ public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType,
+ final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+ final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
+ trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs);
+ }
+ });
+ }
+ }
+
+ public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
+ long loadDurationMs, long bytesLoaded) {
+ loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+ null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+ }
+
+ public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType,
+ final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+ final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
+ final long loadDurationMs, final long bytesLoaded) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat,
+ trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+ }
+ });
+ }
+ }
+
+ public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
+ long loadDurationMs, long bytesLoaded) {
+ loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+ null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+ }
+
+ public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType,
+ final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+ final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
+ final long loadDurationMs, final long bytesLoaded) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat,
+ trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+ }
+ });
+ }
+ }
+
+ public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
+ long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
+ loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+ null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded,
+ error, wasCanceled);
+ }
+
+ public void loadError(final DataSpec dataSpec, final int dataType, final int trackType,
+ final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+ final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
+ final long loadDurationMs, final long bytesLoaded, final IOException error,
+ final boolean wasCanceled) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
+ trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded,
+ error, wasCanceled);
+ }
+ });
+ }
+ }
+
+ public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs,
+ final long mediaEndTimeUs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs),
+ adjustMediaTime(mediaEndTimeUs));
+ }
+ });
+ }
+ }
+
+ public void downstreamFormatChanged(final int trackType, final Format trackFormat,
+ final int trackSelectionReason, final Object trackSelectionData,
+ final long mediaTimeUs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason,
+ trackSelectionData, adjustMediaTime(mediaTimeUs));
+ }
+ });
+ }
+ }
+
+ private long adjustMediaTime(long mediaTimeUs) {
+ long mediaTimeMs = C.usToMs(mediaTimeUs);
+ return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/BehindLiveWindowException.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import java.io.IOException;
+
+/**
+ * Thrown when a live playback falls behind the available media window.
+ */
+public final class BehindLiveWindowException extends IOException {
+
+ public BehindLiveWindowException() {
+ super();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -0,0 +1,271 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their
+ * samples.
+ */
+public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ /**
+ * The {@link MediaPeriod} wrapped by this clipping media period.
+ */
+ public final MediaPeriod mediaPeriod;
+
+ private MediaPeriod.Callback callback;
+ private long startUs;
+ private long endUs;
+ private ClippingSampleStream[] sampleStreams;
+ private boolean pendingInitialDiscontinuity;
+
+ /**
+ * Creates a new clipping media period that provides a clipped view of the specified
+ * {@link MediaPeriod}'s sample streams.
+ * <p>
+ * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)}
+ * on the playback thread before preparation completes.
+ *
+ * @param mediaPeriod The media period to clip.
+ */
+ public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
+ this.mediaPeriod = mediaPeriod;
+ startUs = C.TIME_UNSET;
+ endUs = C.TIME_UNSET;
+ sampleStreams = new ClippingSampleStream[0];
+ }
+
+ /**
+ * Sets the clipping start/end times for this period, in microseconds.
+ *
+ * @param startUs The clipping start time, in microseconds.
+ * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to
+ * indicate the end of the period.
+ */
+ public void setClipping(long startUs, long endUs) {
+ this.startUs = startUs;
+ this.endUs = endUs;
+ }
+
+ @Override
+ public void prepare(MediaPeriod.Callback callback) {
+ this.callback = callback;
+ mediaPeriod.prepare(this);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ mediaPeriod.maybeThrowPrepareError();
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return mediaPeriod.getTrackGroups();
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ sampleStreams = new ClippingSampleStream[streams.length];
+ SampleStream[] internalStreams = new SampleStream[streams.length];
+ for (int i = 0; i < streams.length; i++) {
+ sampleStreams[i] = (ClippingSampleStream) streams[i];
+ internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null;
+ }
+ long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
+ internalStreams, streamResetFlags, positionUs + startUs);
+ Assertions.checkState(enablePositionUs == positionUs + startUs
+ || (enablePositionUs >= startUs
+ && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
+ for (int i = 0; i < streams.length; i++) {
+ if (internalStreams[i] == null) {
+ sampleStreams[i] = null;
+ } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) {
+ sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs,
+ pendingInitialDiscontinuity);
+ }
+ streams[i] = sampleStreams[i];
+ }
+ return enablePositionUs - startUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ mediaPeriod.discardBuffer(positionUs + startUs);
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (pendingInitialDiscontinuity) {
+ for (ClippingSampleStream sampleStream : sampleStreams) {
+ if (sampleStream != null) {
+ sampleStream.clearPendingDiscontinuity();
+ }
+ }
+ pendingInitialDiscontinuity = false;
+ // Always read an initial discontinuity, using mediaPeriod's discontinuity if set.
+ long discontinuityUs = readDiscontinuity();
+ return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0;
+ }
+ long discontinuityUs = mediaPeriod.readDiscontinuity();
+ if (discontinuityUs == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ Assertions.checkState(discontinuityUs >= startUs);
+ Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs);
+ return discontinuityUs - startUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
+ if (bufferedPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) {
+ return C.TIME_END_OF_SOURCE;
+ }
+ return Math.max(0, bufferedPositionUs - startUs);
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ for (ClippingSampleStream sampleStream : sampleStreams) {
+ if (sampleStream != null) {
+ sampleStream.clearSentEos();
+ }
+ }
+ long seekUs = mediaPeriod.seekToUs(positionUs + startUs);
+ Assertions.checkState(seekUs == positionUs + startUs
+ || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
+ return seekUs - startUs;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {
+ return C.TIME_END_OF_SOURCE;
+ }
+ return nextLoadPositionUs - startUs;
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod.continueLoading(positionUs + startUs);
+ }
+
+ // MediaPeriod.Callback implementation.
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
+ // If the clipping start position is non-zero, the clipping sample streams will adjust
+ // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
+ // timestamps can be negative, because sample streams provide buffers starting at a key-frame,
+ // which may be before the clipping start point. When the renderer reads a buffer with a
+ // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp
+ // read in the previous period. Renderer implementations may not allow this, so we signal a
+ // discontinuity which resets the renderers before they read the clipping sample stream.
+ pendingInitialDiscontinuity = startUs != 0;
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ callback.onContinueLoadingRequested(this);
+ }
+
+ /**
+ * Wraps a {@link SampleStream} and clips its samples.
+ */
+ private static final class ClippingSampleStream implements SampleStream {
+
+ private final MediaPeriod mediaPeriod;
+ private final SampleStream stream;
+ private final long startUs;
+ private final long endUs;
+
+ private boolean pendingDiscontinuity;
+ private boolean sentEos;
+
+ public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs,
+ long endUs, boolean pendingDiscontinuity) {
+ this.mediaPeriod = mediaPeriod;
+ this.stream = stream;
+ this.startUs = startUs;
+ this.endUs = endUs;
+ this.pendingDiscontinuity = pendingDiscontinuity;
+ }
+
+ public void clearPendingDiscontinuity() {
+ pendingDiscontinuity = false;
+ }
+
+ public void clearSentEos() {
+ sentEos = false;
+ }
+
+ @Override
+ public boolean isReady() {
+ return stream.isReady();
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ stream.maybeThrowError();
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ if (pendingDiscontinuity) {
+ return C.RESULT_NOTHING_READ;
+ }
+ if (sentEos) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+ int result = stream.readData(formatHolder, buffer, requireFormat);
+ // TODO: Clear gapless playback metadata if a format was read (if applicable).
+ if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
+ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
+ && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
+ buffer.clear();
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ sentEos = true;
+ return C.RESULT_BUFFER_READ;
+ }
+ if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
+ buffer.timeUs -= startUs;
+ }
+ return result;
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ stream.skipData(startUs + positionUs);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -0,0 +1,185 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end
+ * positions. The wrapped source may only have a single period/window and it must not be dynamic
+ * (live).
+ */
+public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
+
+ private final MediaSource mediaSource;
+ private final long startUs;
+ private final long endUs;
+ private final ArrayList<ClippingMediaPeriod> mediaPeriods;
+
+ private MediaSource.Listener sourceListener;
+ private ClippingTimeline clippingTimeline;
+
+ /**
+ * Creates a new clipping source that wraps the specified source.
+ *
+ * @param mediaSource The single-period, non-dynamic source to wrap.
+ * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to
+ * start providing samples, in microseconds.
+ * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop
+ * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples
+ * from the specified start point up to the end of the source.
+ */
+ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {
+ Assertions.checkArgument(startPositionUs >= 0);
+ this.mediaSource = Assertions.checkNotNull(mediaSource);
+ startUs = startPositionUs;
+ endUs = endPositionUs;
+ mediaPeriods = new ArrayList<>();
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ this.sourceListener = listener;
+ mediaSource.prepareSource(player, false, this);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod(
+ mediaSource.createPeriod(index, allocator, startUs + positionUs));
+ mediaPeriods.add(mediaPeriod);
+ mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ Assertions.checkState(mediaPeriods.remove(mediaPeriod));
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ }
+
+ @Override
+ public void releaseSource() {
+ mediaSource.releaseSource();
+ }
+
+ // MediaSource.Listener implementation.
+
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ clippingTimeline = new ClippingTimeline(timeline, startUs, endUs);
+ sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest);
+ long startUs = clippingTimeline.startUs;
+ long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE
+ : clippingTimeline.endUs;
+ int count = mediaPeriods.size();
+ for (int i = 0; i < count; i++) {
+ mediaPeriods.get(i).setClipping(startUs, endUs);
+ }
+ }
+
+ /**
+ * Provides a clipped view of a specified timeline.
+ */
+ private static final class ClippingTimeline extends Timeline {
+
+ private final Timeline timeline;
+ private final long startUs;
+ private final long endUs;
+
+ /**
+ * Creates a new clipping timeline that wraps the specified timeline.
+ *
+ * @param timeline The timeline to clip.
+ * @param startUs The number of microseconds to clip from the start of {@code timeline}.
+ * @param endUs The end position in microseconds for the clipped timeline relative to the start
+ * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end.
+ */
+ public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
+ Assertions.checkArgument(timeline.getWindowCount() == 1);
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ Window window = timeline.getWindow(0, new Window(), false);
+ Assertions.checkArgument(!window.isDynamic);
+ long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs;
+ if (window.durationUs != C.TIME_UNSET) {
+ Assertions.checkArgument(startUs == 0 || window.isSeekable);
+ Assertions.checkArgument(resolvedEndUs <= window.durationUs);
+ Assertions.checkArgument(startUs <= resolvedEndUs);
+ }
+ Period period = timeline.getPeriod(0, new Period());
+ Assertions.checkArgument(period.getPositionInWindowUs() == 0);
+ this.timeline = timeline;
+ this.startUs = startUs;
+ this.endUs = resolvedEndUs;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs);
+ window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET;
+ if (window.defaultPositionUs != C.TIME_UNSET) {
+ window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs);
+ window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs
+ : Math.min(window.defaultPositionUs, endUs);
+ window.defaultPositionUs -= startUs;
+ }
+ long startMs = C.usToMs(startUs);
+ if (window.presentationStartTimeMs != C.TIME_UNSET) {
+ window.presentationStartTimeMs += startMs;
+ }
+ if (window.windowStartTimeMs != C.TIME_UNSET) {
+ window.windowStartTimeMs += startMs;
+ }
+ return window;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ period = timeline.getPeriod(0, period, setIds);
+ period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET;
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return timeline.getIndexOfPeriod(uid);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s.
+ */
+public final class CompositeSequenceableLoader implements SequenceableLoader {
+
+ private final SequenceableLoader[] loaders;
+
+ public CompositeSequenceableLoader(SequenceableLoader[] loaders) {
+ this.loaders = loaders;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ long nextLoadPositionUs = Long.MAX_VALUE;
+ for (SequenceableLoader loader : loaders) {
+ long loaderNextLoadPositionUs = loader.getNextLoadPositionUs();
+ if (loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE) {
+ nextLoadPositionUs = Math.min(nextLoadPositionUs, loaderNextLoadPositionUs);
+ }
+ }
+ return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs;
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ boolean madeProgress = false;
+ boolean madeProgressThisIteration;
+ do {
+ madeProgressThisIteration = false;
+ long nextLoadPositionUs = getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ break;
+ }
+ for (SequenceableLoader loader : loaders) {
+ if (loader.getNextLoadPositionUs() == nextLoadPositionUs) {
+ madeProgressThisIteration |= loader.continueLoading(positionUs);
+ }
+ }
+ madeProgress |= madeProgressThisIteration;
+ } while (madeProgressThisIteration);
+ return madeProgress;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -0,0 +1,245 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance
+ * to be present more than once in the concatenation.
+ */
+public final class ConcatenatingMediaSource implements MediaSource {
+
+ private final MediaSource[] mediaSources;
+ private final Timeline[] timelines;
+ private final Object[] manifests;
+ private final Map<MediaPeriod, Integer> sourceIndexByMediaPeriod;
+ private final boolean[] duplicateFlags;
+
+ private Listener listener;
+ private ConcatenatedTimeline timeline;
+
+ /**
+ * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
+ * {@link MediaSource} instance to be present more than once in the array.
+ */
+ public ConcatenatingMediaSource(MediaSource... mediaSources) {
+ this.mediaSources = mediaSources;
+ timelines = new Timeline[mediaSources.length];
+ manifests = new Object[mediaSources.length];
+ sourceIndexByMediaPeriod = new HashMap<>();
+ duplicateFlags = buildDuplicateFlags(mediaSources);
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ this.listener = listener;
+ for (int i = 0; i < mediaSources.length; i++) {
+ if (!duplicateFlags[i]) {
+ final int index = i;
+ mediaSources[i].prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ handleSourceInfoRefreshed(index, timeline, manifest);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ for (int i = 0; i < mediaSources.length; i++) {
+ if (!duplicateFlags[i]) {
+ mediaSources[i].maybeThrowSourceInfoRefreshError();
+ }
+ }
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ int sourceIndex = timeline.getSourceIndexForPeriod(index);
+ int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex);
+ MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator,
+ positionUs);
+ sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
+ return mediaPeriod;
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod);
+ sourceIndexByMediaPeriod.remove(mediaPeriod);
+ mediaSources[sourceIndex].releasePeriod(mediaPeriod);
+ }
+
+ @Override
+ public void releaseSource() {
+ for (int i = 0; i < mediaSources.length; i++) {
+ if (!duplicateFlags[i]) {
+ mediaSources[i].releaseSource();
+ }
+ }
+ }
+
+ private void handleSourceInfoRefreshed(int sourceFirstIndex, Timeline sourceTimeline,
+ Object sourceManifest) {
+ // Set the timeline and manifest.
+ timelines[sourceFirstIndex] = sourceTimeline;
+ manifests[sourceFirstIndex] = sourceManifest;
+ // Also set the timeline and manifest for any duplicate entries of the same source.
+ for (int i = sourceFirstIndex + 1; i < mediaSources.length; i++) {
+ if (mediaSources[i] == mediaSources[sourceFirstIndex]) {
+ timelines[i] = sourceTimeline;
+ manifests[i] = sourceManifest;
+ }
+ }
+ for (Timeline timeline : timelines) {
+ if (timeline == null) {
+ // Don't invoke the listener until all sources have timelines.
+ return;
+ }
+ }
+ timeline = new ConcatenatedTimeline(timelines.clone());
+ listener.onSourceInfoRefreshed(timeline, manifests.clone());
+ }
+
+ private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) {
+ boolean[] duplicateFlags = new boolean[mediaSources.length];
+ IdentityHashMap<MediaSource, Void> sources = new IdentityHashMap<>(mediaSources.length);
+ for (int i = 0; i < mediaSources.length; i++) {
+ MediaSource source = mediaSources[i];
+ if (!sources.containsKey(source)) {
+ sources.put(source, null);
+ } else {
+ duplicateFlags[i] = true;
+ }
+ }
+ return duplicateFlags;
+ }
+
+ /**
+ * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
+ */
+ private static final class ConcatenatedTimeline extends Timeline {
+
+ private final Timeline[] timelines;
+ private final int[] sourcePeriodOffsets;
+ private final int[] sourceWindowOffsets;
+
+ public ConcatenatedTimeline(Timeline[] timelines) {
+ int[] sourcePeriodOffsets = new int[timelines.length];
+ int[] sourceWindowOffsets = new int[timelines.length];
+ long periodCount = 0;
+ int windowCount = 0;
+ for (int i = 0; i < timelines.length; i++) {
+ Timeline timeline = timelines[i];
+ periodCount += timeline.getPeriodCount();
+ Assertions.checkState(periodCount <= Integer.MAX_VALUE,
+ "ConcatenatingMediaSource children contain too many periods");
+ sourcePeriodOffsets[i] = (int) periodCount;
+ windowCount += timeline.getWindowCount();
+ sourceWindowOffsets[i] = windowCount;
+ }
+ this.timelines = timelines;
+ this.sourcePeriodOffsets = sourcePeriodOffsets;
+ this.sourceWindowOffsets = sourceWindowOffsets;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return sourceWindowOffsets[sourceWindowOffsets.length - 1];
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ int sourceIndex = getSourceIndexForWindow(windowIndex);
+ int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
+ int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
+ timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds,
+ defaultPositionProjectionUs);
+ window.firstPeriodIndex += firstPeriodIndexInSource;
+ window.lastPeriodIndex += firstPeriodIndexInSource;
+ return window;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return sourcePeriodOffsets[sourcePeriodOffsets.length - 1];
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ int sourceIndex = getSourceIndexForPeriod(periodIndex);
+ int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
+ int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
+ timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds);
+ period.windowIndex += firstWindowIndexInSource;
+ if (setIds) {
+ period.uid = Pair.create(sourceIndex, period.uid);
+ }
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ if (!(uid instanceof Pair)) {
+ return C.INDEX_UNSET;
+ }
+ Pair<?, ?> sourceIndexAndPeriodId = (Pair<?, ?>) uid;
+ if (!(sourceIndexAndPeriodId.first instanceof Integer)) {
+ return C.INDEX_UNSET;
+ }
+ int sourceIndex = (Integer) sourceIndexAndPeriodId.first;
+ Object periodId = sourceIndexAndPeriodId.second;
+ if (sourceIndex < 0 || sourceIndex >= timelines.length) {
+ return C.INDEX_UNSET;
+ }
+ int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId);
+ return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET
+ : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource;
+ }
+
+ private int getSourceIndexForPeriod(int periodIndex) {
+ return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1;
+ }
+
+ private int getFirstPeriodIndexInSource(int sourceIndex) {
+ return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1];
+ }
+
+ private int getSourceIndexForWindow(int windowIndex) {
+ return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1;
+ }
+
+ private int getFirstWindowIndexInSource(int sourceIndex) {
+ return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1];
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/EmptySampleStream.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import java.io.IOException;
+
+/**
+ * An empty {@link SampleStream}.
+ */
+public final class EmptySampleStream implements SampleStream {
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ // Do nothing.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -0,0 +1,737 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ConditionVariable;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * A {@link MediaPeriod} that extracts data using an {@link Extractor}.
+ */
+/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
+ Loader.Callback<ExtractorMediaPeriod.ExtractingLoadable>, UpstreamFormatChangedListener {
+
+ /**
+ * When the source's duration is unknown, it is calculated by adding this value to the largest
+ * sample timestamp seen when buffering completes.
+ */
+ private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000;
+
+ private final Uri uri;
+ private final DataSource dataSource;
+ private final int minLoadableRetryCount;
+ private final Handler eventHandler;
+ private final ExtractorMediaSource.EventListener eventListener;
+ private final MediaSource.Listener sourceListener;
+ private final Allocator allocator;
+ private final String customCacheKey;
+ private final Loader loader;
+ private final ExtractorHolder extractorHolder;
+ private final ConditionVariable loadCondition;
+ private final Runnable maybeFinishPrepareRunnable;
+ private final Runnable onContinueLoadingRequestedRunnable;
+ private final Handler handler;
+ private final SparseArray<DefaultTrackOutput> sampleQueues;
+
+ private Callback callback;
+ private SeekMap seekMap;
+ private boolean tracksBuilt;
+ private boolean prepared;
+
+ private boolean seenFirstTrackSelection;
+ private boolean notifyReset;
+ private int enabledTrackCount;
+ private TrackGroupArray tracks;
+ private long durationUs;
+ private boolean[] trackEnabledStates;
+ private boolean[] trackIsAudioVideoFlags;
+ private boolean haveAudioVideoTracks;
+ private long length;
+
+ private long lastSeekPositionUs;
+ private long pendingResetPositionUs;
+
+ private int extractedSamplesCountAtStartOfLoad;
+ private boolean loadingFinished;
+ private boolean released;
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSource The data source to read the media.
+ * @param extractors The extractors to use to read the data source.
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param sourceListener A listener to notify when the timeline has been loaded.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ */
+ public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
+ int minLoadableRetryCount, Handler eventHandler,
+ ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
+ Allocator allocator, String customCacheKey) {
+ this.uri = uri;
+ this.dataSource = dataSource;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.sourceListener = sourceListener;
+ this.allocator = allocator;
+ this.customCacheKey = customCacheKey;
+ loader = new Loader("Loader:ExtractorMediaPeriod");
+ extractorHolder = new ExtractorHolder(extractors, this);
+ loadCondition = new ConditionVariable();
+ maybeFinishPrepareRunnable = new Runnable() {
+ @Override
+ public void run() {
+ maybeFinishPrepare();
+ }
+ };
+ onContinueLoadingRequestedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!released) {
+ callback.onContinueLoadingRequested(ExtractorMediaPeriod.this);
+ }
+ }
+ };
+ handler = new Handler();
+
+ pendingResetPositionUs = C.TIME_UNSET;
+ sampleQueues = new SparseArray<>();
+ length = C.LENGTH_UNSET;
+ }
+
+ public void release() {
+ final ExtractorHolder extractorHolder = this.extractorHolder;
+ loader.release(new Runnable() {
+ @Override
+ public void run() {
+ extractorHolder.release();
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues.valueAt(i).disable();
+ }
+ }
+ });
+ handler.removeCallbacksAndMessages(null);
+ released = true;
+ }
+
+ @Override
+ public void prepare(Callback callback) {
+ this.callback = callback;
+ loadCondition.open();
+ startLoading();
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ maybeThrowError();
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return tracks;
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ Assertions.checkState(prepared);
+ // Disable old tracks.
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ int track = ((SampleStreamImpl) streams[i]).track;
+ Assertions.checkState(trackEnabledStates[track]);
+ enabledTrackCount--;
+ trackEnabledStates[track] = false;
+ sampleQueues.valueAt(track).disable();
+ streams[i] = null;
+ }
+ }
+ // Enable new tracks.
+ boolean selectedNewTracks = false;
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] == null && selections[i] != null) {
+ TrackSelection selection = selections[i];
+ Assertions.checkState(selection.length() == 1);
+ Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
+ int track = tracks.indexOf(selection.getTrackGroup());
+ Assertions.checkState(!trackEnabledStates[track]);
+ enabledTrackCount++;
+ trackEnabledStates[track] = true;
+ streams[i] = new SampleStreamImpl(track);
+ streamResetFlags[i] = true;
+ selectedNewTracks = true;
+ }
+ }
+ if (!seenFirstTrackSelection) {
+ // At the time of the first track selection all queues will be enabled, so we need to disable
+ // any that are no longer required.
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ if (!trackEnabledStates[i]) {
+ sampleQueues.valueAt(i).disable();
+ }
+ }
+ }
+ if (enabledTrackCount == 0) {
+ notifyReset = false;
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ }
+ } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) {
+ positionUs = seekToUs(positionUs);
+ // We'll need to reset renderers consuming from all streams due to the seek.
+ for (int i = 0; i < streams.length; i++) {
+ if (streams[i] != null) {
+ streamResetFlags[i] = true;
+ }
+ }
+ }
+ seenFirstTrackSelection = true;
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean continueLoading(long playbackPositionUs) {
+ if (loadingFinished || (prepared && enabledTrackCount == 0)) {
+ return false;
+ }
+ boolean continuedLoading = loadCondition.open();
+ if (!loader.isLoading()) {
+ startLoading();
+ continuedLoading = true;
+ }
+ return continuedLoading;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ if (notifyReset) {
+ notifyReset = false;
+ return lastSeekPositionUs;
+ }
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ }
+ long largestQueuedTimestampUs;
+ if (haveAudioVideoTracks) {
+ // Ignore non-AV tracks, which may be sparse or poorly interleaved.
+ largestQueuedTimestampUs = Long.MAX_VALUE;
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ if (trackIsAudioVideoFlags[i]) {
+ largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
+ sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ }
+ }
+ } else {
+ largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+ }
+ return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
+ : largestQueuedTimestampUs;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ // Treat all seeks into non-seekable media as being to t=0.
+ positionUs = seekMap.isSeekable() ? positionUs : 0;
+ lastSeekPositionUs = positionUs;
+ int trackCount = sampleQueues.size();
+ // If we're not pending a reset, see if we can seek within the sample queues.
+ boolean seekInsideBuffer = !isPendingReset();
+ for (int i = 0; seekInsideBuffer && i < trackCount; i++) {
+ if (trackEnabledStates[i]) {
+ seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false);
+ }
+ }
+ // If we failed to seek within the sample queues, we need to restart.
+ if (!seekInsideBuffer) {
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
+ }
+ }
+ }
+ notifyReset = false;
+ return positionUs;
+ }
+
+ // SampleStream methods.
+
+ /* package */ boolean isReady(int track) {
+ return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
+ }
+
+ /* package */ void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ }
+
+ /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (notifyReset || isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+
+ return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired,
+ loadingFinished, lastSeekPositionUs);
+ }
+
+ /* package */ void skipData(int track, long positionUs) {
+ DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track);
+ if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
+ sampleQueue.skipAll();
+ } else {
+ sampleQueue.skipToKeyframeBefore(positionUs, true);
+ }
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs) {
+ copyLengthFromLoader(loadable);
+ loadingFinished = true;
+ if (durationUs == C.TIME_UNSET) {
+ long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+ durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
+ : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
+ sourceListener.onSourceInfoRefreshed(
+ new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null);
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs, boolean released) {
+ copyLengthFromLoader(loadable);
+ if (!released && enabledTrackCount > 0) {
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs, IOException error) {
+ copyLengthFromLoader(loadable);
+ notifyLoadError(error);
+ if (isLoadableExceptionFatal(error)) {
+ return Loader.DONT_RETRY_FATAL;
+ }
+ int extractedSamplesCount = getExtractedSamplesCount();
+ boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad;
+ configureRetry(loadable); // May reset the sample queues.
+ extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
+ return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY;
+ }
+
+ // ExtractorOutput implementation. Called by the loading thread.
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ DefaultTrackOutput trackOutput = sampleQueues.get(id);
+ if (trackOutput == null) {
+ trackOutput = new DefaultTrackOutput(allocator);
+ trackOutput.setUpstreamFormatChangeListener(this);
+ sampleQueues.put(id, trackOutput);
+ }
+ return trackOutput;
+ }
+
+ @Override
+ public void endTracks() {
+ tracksBuilt = true;
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ this.seekMap = seekMap;
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+ @Override
+ public void onUpstreamFormatChanged(Format format) {
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ // Internal methods.
+
+ private void maybeFinishPrepare() {
+ if (released || prepared || seekMap == null || !tracksBuilt) {
+ return;
+ }
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
+ return;
+ }
+ }
+ loadCondition.close();
+ TrackGroup[] trackArray = new TrackGroup[trackCount];
+ trackIsAudioVideoFlags = new boolean[trackCount];
+ trackEnabledStates = new boolean[trackCount];
+ durationUs = seekMap.getDurationUs();
+ for (int i = 0; i < trackCount; i++) {
+ Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat();
+ trackArray[i] = new TrackGroup(trackFormat);
+ String mimeType = trackFormat.sampleMimeType;
+ boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
+ trackIsAudioVideoFlags[i] = isAudioVideo;
+ haveAudioVideoTracks |= isAudioVideo;
+ }
+ tracks = new TrackGroupArray(trackArray);
+ prepared = true;
+ sourceListener.onSourceInfoRefreshed(
+ new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null);
+ callback.onPrepared(this);
+ }
+
+ private void copyLengthFromLoader(ExtractingLoadable loadable) {
+ if (length == C.LENGTH_UNSET) {
+ length = loadable.length;
+ }
+ }
+
+ private void startLoading() {
+ ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder,
+ loadCondition);
+ if (prepared) {
+ Assertions.checkState(isPendingReset());
+ if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) {
+ loadingFinished = true;
+ pendingResetPositionUs = C.TIME_UNSET;
+ return;
+ }
+ loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs);
+ pendingResetPositionUs = C.TIME_UNSET;
+ }
+ extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
+
+ int minRetryCount = minLoadableRetryCount;
+ if (minRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) {
+ // We assume on-demand before we're prepared.
+ minRetryCount = !prepared || length != C.LENGTH_UNSET
+ || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)
+ ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND
+ : ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE;
+ }
+ loader.startLoading(loadable, this, minRetryCount);
+ }
+
+ private void configureRetry(ExtractingLoadable loadable) {
+ if (length != C.LENGTH_UNSET
+ || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
+ // We're playing an on-demand stream. Resume the current loadable, which will
+ // request data starting from the point it left off.
+ } else {
+ // We're playing a stream of unknown length and duration. Assume it's live, and
+ // therefore that the data at the uri is a continuously shifting window of the latest
+ // available media. For this case there's no way to continue loading from where a
+ // previous load finished, so it's necessary to load from the start whenever commencing
+ // a new load.
+ lastSeekPositionUs = 0;
+ notifyReset = prepared;
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
+ }
+ loadable.setLoadPosition(0, 0);
+ }
+ }
+
+ private int getExtractedSamplesCount() {
+ int extractedSamplesCount = 0;
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
+ }
+ return extractedSamplesCount;
+ }
+
+ private long getLargestQueuedTimestampUs() {
+ long largestQueuedTimestampUs = Long.MIN_VALUE;
+ int trackCount = sampleQueues.size();
+ for (int i = 0; i < trackCount; i++) {
+ largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
+ sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ }
+ return largestQueuedTimestampUs;
+ }
+
+ private boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ private boolean isLoadableExceptionFatal(IOException e) {
+ return e instanceof UnrecognizedInputFormatException;
+ }
+
+ private void notifyLoadError(final IOException error) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onLoadError(error);
+ }
+ });
+ }
+ }
+
+ private final class SampleStreamImpl implements SampleStream {
+
+ private final int track;
+
+ public SampleStreamImpl(int track) {
+ this.track = track;
+ }
+
+ @Override
+ public boolean isReady() {
+ return ExtractorMediaPeriod.this.isReady(track);
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ ExtractorMediaPeriod.this.maybeThrowError();
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ ExtractorMediaPeriod.this.skipData(track, positionUs);
+ }
+
+ }
+
+ /**
+ * Loads the media stream and extracts sample data from it.
+ */
+ /* package */ final class ExtractingLoadable implements Loadable {
+
+ /**
+ * The number of bytes that should be loaded between each each invocation of
+ * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
+ */
+ private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
+
+ private final Uri uri;
+ private final DataSource dataSource;
+ private final ExtractorHolder extractorHolder;
+ private final ConditionVariable loadCondition;
+ private final PositionHolder positionHolder;
+
+ private volatile boolean loadCanceled;
+
+ private boolean pendingExtractorSeek;
+ private long seekTimeUs;
+ private long length;
+
+ public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
+ ConditionVariable loadCondition) {
+ this.uri = Assertions.checkNotNull(uri);
+ this.dataSource = Assertions.checkNotNull(dataSource);
+ this.extractorHolder = Assertions.checkNotNull(extractorHolder);
+ this.loadCondition = loadCondition;
+ this.positionHolder = new PositionHolder();
+ this.pendingExtractorSeek = true;
+ this.length = C.LENGTH_UNSET;
+ }
+
+ public void setLoadPosition(long position, long timeUs) {
+ positionHolder.position = position;
+ seekTimeUs = timeUs;
+ pendingExtractorSeek = true;
+ }
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ ExtractorInput input = null;
+ try {
+ long position = positionHolder.position;
+ length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey));
+ if (length != C.LENGTH_UNSET) {
+ length += position;
+ }
+ input = new DefaultExtractorInput(dataSource, position, length);
+ Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri());
+ if (pendingExtractorSeek) {
+ extractor.seek(position, seekTimeUs);
+ pendingExtractorSeek = false;
+ }
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ loadCondition.block();
+ result = extractor.read(input, positionHolder);
+ if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) {
+ position = input.getPosition();
+ loadCondition.close();
+ handler.post(onContinueLoadingRequestedRunnable);
+ }
+ }
+ } finally {
+ if (result == Extractor.RESULT_SEEK) {
+ result = Extractor.RESULT_CONTINUE;
+ } else if (input != null) {
+ positionHolder.position = input.getPosition();
+ }
+ Util.closeQuietly(dataSource);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Stores a list of extractors and a selected extractor when the format has been detected.
+ */
+ private static final class ExtractorHolder {
+
+ private final Extractor[] extractors;
+ private final ExtractorOutput extractorOutput;
+ private Extractor extractor;
+
+ /**
+ * Creates a holder that will select an extractor and initialize it using the specified output.
+ *
+ * @param extractors One or more extractors to choose from.
+ * @param extractorOutput The output that will be used to initialize the selected extractor.
+ */
+ public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) {
+ this.extractors = extractors;
+ this.extractorOutput = extractorOutput;
+ }
+
+ /**
+ * Returns an initialized extractor for reading {@code input}, and returns the same extractor on
+ * later calls.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param uri The {@link Uri} of the data.
+ * @return An initialized extractor for reading {@code input}.
+ * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.
+ * @throws IOException Thrown if the input could not be read.
+ * @throws InterruptedException Thrown if the thread was interrupted.
+ */
+ public Extractor selectExtractor(ExtractorInput input, Uri uri)
+ throws IOException, InterruptedException {
+ if (extractor != null) {
+ return extractor;
+ }
+ for (Extractor extractor : extractors) {
+ try {
+ if (extractor.sniff(input)) {
+ this.extractor = extractor;
+ break;
+ }
+ } catch (EOFException e) {
+ // Do nothing.
+ } finally {
+ input.resetPeekPosition();
+ }
+ }
+ if (extractor == null) {
+ throw new UnrecognizedInputFormatException("None of the available extractors ("
+ + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri);
+ }
+ extractor.init(extractorOutput);
+ return extractor;
+ }
+
+ public void release() {
+ if (extractor != null) {
+ extractor.release();
+ extractor = null;
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -0,0 +1,191 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
+ * <p>
+ * If the possible input stream container formats are known, pass a factory that instantiates
+ * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to
+ * use the default extractors. When reading a new stream, the first {@link Extractor} in the array
+ * of extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will
+ * be used to extract samples from the input stream.
+ * <p>
+ * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
+ */
+public final class ExtractorMediaSource implements MediaSource, MediaSource.Listener {
+
+ /**
+ * Listener of {@link ExtractorMediaSource} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Called when an error occurs loading media data.
+ *
+ * @param error The load error.
+ */
+ void onLoadError(IOException error);
+
+ }
+
+ /**
+ * The default minimum number of times to retry loading prior to failing for on-demand streams.
+ */
+ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND = 3;
+
+ /**
+ * The default minimum number of times to retry loading prior to failing for live streams.
+ */
+ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6;
+
+ /**
+ * Value for {@code minLoadableRetryCount} that causes the loader to retry
+ * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE} times for live streams and
+ * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND} for on-demand streams.
+ */
+ public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1;
+
+ private final Uri uri;
+ private final DataSource.Factory dataSourceFactory;
+ private final ExtractorsFactory extractorsFactory;
+ private final int minLoadableRetryCount;
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final Timeline.Period period;
+ private final String customCacheKey;
+
+ private MediaSource.Listener sourceListener;
+ private Timeline timeline;
+ private boolean timelineHasDuration;
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those formats.
+ * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
+ this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
+ eventListener, null);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those formats.
+ * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ */
+ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
+ String customCacheKey) {
+ this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
+ eventListener, customCacheKey);
+ }
+
+ /**
+ * @param uri The {@link Uri} of the media stream.
+ * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
+ * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
+ * possible formats are known, pass a factory that instantiates extractors for those formats.
+ * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
+ * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+ * @param eventHandler A handler for events. May be null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ */
+ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
+ ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
+ EventListener eventListener, String customCacheKey) {
+ this.uri = uri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.extractorsFactory = extractorsFactory;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.customCacheKey = customCacheKey;
+ period = new Timeline.Period();
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ sourceListener = listener;
+ timeline = new SinglePeriodTimeline(C.TIME_UNSET, false);
+ listener.onSourceInfoRefreshed(timeline, null);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ Assertions.checkArgument(index == 0);
+ return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
+ extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
+ this, allocator, customCacheKey);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((ExtractorMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ public void releaseSource() {
+ sourceListener = null;
+ }
+
+ // MediaSource.Listener implementation.
+
+ @Override
+ public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
+ long newTimelineDurationUs = newTimeline.getPeriod(0, period).getDurationUs();
+ boolean newTimelineHasDuration = newTimelineDurationUs != C.TIME_UNSET;
+ if (timelineHasDuration && !newTimelineHasDuration) {
+ // Suppress source info changes that would make the duration unknown when it is already known.
+ return;
+ }
+ timeline = newTimeline;
+ timelineHasDuration = newTimelineHasDuration;
+ sourceListener.onSourceInfoRefreshed(timeline, null);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -0,0 +1,172 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Loops a {@link MediaSource}.
+ */
+public final class LoopingMediaSource implements MediaSource {
+
+ /**
+ * The maximum number of periods that can be exposed by the source. The value of this constant is
+ * large enough to cause indefinite looping in practice (the total duration of the looping source
+ * will be approximately five years if the duration of each period is one second).
+ */
+ public static final int MAX_EXPOSED_PERIODS = 157680000;
+
+ private static final String TAG = "LoopingMediaSource";
+
+ private final MediaSource childSource;
+ private final int loopCount;
+
+ private int childPeriodCount;
+
+ /**
+ * Loops the provided source indefinitely.
+ *
+ * @param childSource The {@link MediaSource} to loop.
+ */
+ public LoopingMediaSource(MediaSource childSource) {
+ this(childSource, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Loops the provided source a specified number of times.
+ *
+ * @param childSource The {@link MediaSource} to loop.
+ * @param loopCount The desired number of loops. Must be strictly positive. The actual number of
+ * loops will be capped at the maximum that can achieved without causing the number of
+ * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}.
+ */
+ public LoopingMediaSource(MediaSource childSource, int loopCount) {
+ Assertions.checkArgument(loopCount > 0);
+ this.childSource = childSource;
+ this.loopCount = loopCount;
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
+ childSource.prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ childPeriodCount = timeline.getPeriodCount();
+ listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest);
+ }
+ });
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ childSource.maybeThrowSourceInfoRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ return childSource.createPeriod(index % childPeriodCount, allocator, positionUs);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ childSource.releasePeriod(mediaPeriod);
+ }
+
+ @Override
+ public void releaseSource() {
+ childSource.releaseSource();
+ }
+
+ private static final class LoopingTimeline extends Timeline {
+
+ private final Timeline childTimeline;
+ private final int childPeriodCount;
+ private final int childWindowCount;
+ private final int loopCount;
+
+ public LoopingTimeline(Timeline childTimeline, int loopCount) {
+ this.childTimeline = childTimeline;
+ childPeriodCount = childTimeline.getPeriodCount();
+ childWindowCount = childTimeline.getWindowCount();
+ // This is the maximum number of loops that can be performed without exceeding
+ // MAX_EXPOSED_PERIODS periods.
+ int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount;
+ if (loopCount > maxLoopCount) {
+ if (loopCount != Integer.MAX_VALUE) {
+ Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount);
+ }
+ this.loopCount = maxLoopCount;
+ } else {
+ this.loopCount = loopCount;
+ }
+ }
+
+ @Override
+ public int getWindowCount() {
+ return childWindowCount * loopCount;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ childTimeline.getWindow(windowIndex % childWindowCount, window, setIds,
+ defaultPositionProjectionUs);
+ int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount;
+ window.firstPeriodIndex += periodIndexOffset;
+ window.lastPeriodIndex += periodIndexOffset;
+ return window;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return childPeriodCount * loopCount;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds);
+ int loopCount = (periodIndex / childPeriodCount);
+ period.windowIndex += loopCount * childWindowCount;
+ if (setIds) {
+ period.uid = Pair.create(loopCount, period.uid);
+ }
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ if (!(uid instanceof Pair)) {
+ return C.INDEX_UNSET;
+ }
+ Pair<?, ?> loopCountAndChildUid = (Pair<?, ?>) uid;
+ if (!(loopCountAndChildUid.first instanceof Integer)) {
+ return C.INDEX_UNSET;
+ }
+ int loopCount = (Integer) loopCountAndChildUid.first;
+ int periodIndexOffset = loopCount * childPeriodCount;
+ return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -0,0 +1,176 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import java.io.IOException;
+
+/**
+ * A source of a single period of media.
+ */
+public interface MediaPeriod extends SequenceableLoader {
+
+ /**
+ * A callback to be notified of {@link MediaPeriod} events.
+ */
+ interface Callback extends SequenceableLoader.Callback<MediaPeriod> {
+
+ /**
+ * Called when preparation completes.
+ * <p>
+ * Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect
+ * for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be
+ * called with the initial track selection.
+ *
+ * @param mediaPeriod The prepared {@link MediaPeriod}.
+ */
+ void onPrepared(MediaPeriod mediaPeriod);
+
+ }
+
+ /**
+ * Prepares this media period asynchronously.
+ * <p>
+ * {@code callback.onPrepared} is called when preparation completes. If preparation fails,
+ * {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
+ * <p>
+ * If preparation succeeds and results in a source timeline change (e.g. the period duration
+ * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} will be
+ * called before {@code callback.onPrepared}.
+ *
+ * @param callback Callback to receive updates from this period, including being notified when
+ * preparation completes.
+ */
+ void prepare(Callback callback);
+
+ /**
+ * Throws an error that's preventing the period from becoming prepared. Does nothing if no such
+ * error exists.
+ * <p>
+ * This method should only be called before the period has completed preparation.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowPrepareError() throws IOException;
+
+ /**
+ * Returns the {@link TrackGroup}s exposed by the period.
+ * <p>
+ * This method should only be called after the period has been prepared.
+ *
+ * @return The {@link TrackGroup}s.
+ */
+ TrackGroupArray getTrackGroups();
+
+ /**
+ * Performs a track selection.
+ * <p>
+ * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
+ * indicating whether the existing {@code SampleStream} can be retained for each selection, and
+ * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
+ * provided selections, clearing, setting and replacing entries as required. If an existing sample
+ * stream is retained but with the requirement that the consuming renderer be reset, then the
+ * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
+ * if a new sample stream is created.
+ * <p>
+ * This method should only be called after the period has been prepared.
+ *
+ * @param selections The renderer track selections.
+ * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
+ * for each selection. A {@code true} value indicates that the selection is unchanged, and
+ * that the caller does not require that the sample stream be recreated.
+ * @param streams The existing sample streams, which will be updated to reflect the provided
+ * selections.
+ * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
+ * have been retained but with the requirement that the consuming renderer be reset.
+ * @param positionUs The current playback position in microseconds.
+ * @return The actual position at which the tracks were enabled, in microseconds.
+ */
+ long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
+
+ /**
+ * Discards buffered media up to the specified position.
+ *
+ * @param positionUs The position in microseconds.
+ */
+ void discardBuffer(long positionUs);
+
+ /**
+ * Attempts to read a discontinuity.
+ * <p>
+ * After this method has returned a value other than {@link C#TIME_UNSET}, all
+ * {@link SampleStream}s provided by the period are guaranteed to start from a key frame.
+ *
+ * @return If a discontinuity was read then the playback position in microseconds after the
+ * discontinuity. Else {@link C#TIME_UNSET}.
+ */
+ long readDiscontinuity();
+
+ /**
+ * Returns an estimate of the position up to which data is buffered for the enabled tracks.
+ * <p>
+ * This method should only be called when at least one track is selected.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
+ */
+ long getBufferedPositionUs();
+
+ /**
+ * Attempts to seek to the specified position in microseconds.
+ * <p>
+ * After this method has been called, all {@link SampleStream}s provided by the period are
+ * guaranteed to start from a key frame.
+ * <p>
+ * This method should only be called when at least one track is selected.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @return The actual position to which the period was seeked, in microseconds.
+ */
+ long seekToUs(long positionUs);
+
+ // SequenceableLoader interface. Overridden to provide more specific documentation.
+
+ /**
+ * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
+ * <p>
+ * This method should only be called after the period has been prepared. It may be called when no
+ * tracks are selected.
+ */
+ @Override
+ long getNextLoadPositionUs();
+
+ /**
+ * Attempts to continue loading.
+ * <p>
+ * This method may be called both during and after the period has been prepared.
+ * <p>
+ * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
+ * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called
+ * when the period is permitted to continue loading data. A period may do this both during and
+ * after preparation.
+ *
+ * @param positionUs The current playback position.
+ * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
+ * a different value than prior to the call. False otherwise.
+ */
+ @Override
+ boolean continueLoading(long positionUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+
+/**
+ * A source of media consisting of one or more {@link MediaPeriod}s.
+ */
+public interface MediaSource {
+
+ /**
+ * Listener for source events.
+ */
+ interface Listener {
+
+ /**
+ * Called when manifest and/or timeline has been refreshed.
+ *
+ * @param timeline The source's timeline.
+ * @param manifest The loaded manifest.
+ */
+ void onSourceInfoRefreshed(Timeline timeline, Object manifest);
+
+ }
+
+ /**
+ * Starts preparation of the source.
+ *
+ * @param player The player for which this source is being prepared.
+ * @param isTopLevelSource Whether this source has been passed directly to
+ * {@link ExoPlayer#prepare(MediaSource)} or
+ * {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is
+ * being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition.
+ * @param listener The listener for source events.
+ */
+ void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener);
+
+ /**
+ * Throws any pending error encountered while loading or refreshing source information.
+ */
+ void maybeThrowSourceInfoRefreshError() throws IOException;
+
+ /**
+ * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}.
+ * This method may be called multiple times with the same index without an intervening call to
+ * {@link #releasePeriod(MediaPeriod)}.
+ *
+ * @param index The index of the period.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param positionUs The player's current playback position.
+ * @return A new {@link MediaPeriod}.
+ */
+ MediaPeriod createPeriod(int index, Allocator allocator, long positionUs);
+
+ /**
+ * Releases the period.
+ *
+ * @param mediaPeriod The period to release.
+ */
+ void releasePeriod(MediaPeriod mediaPeriod);
+
+ /**
+ * Releases the source.
+ * <p>
+ * This method should be called when the source is no longer required. It may be called in any
+ * state.
+ */
+ void releaseSource();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -0,0 +1,226 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+
+/**
+ * Merges multiple {@link MediaPeriod}s.
+ */
+/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ public final MediaPeriod[] periods;
+
+ private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;
+
+ private Callback callback;
+ private int pendingChildPrepareCount;
+ private TrackGroupArray trackGroups;
+
+ private MediaPeriod[] enabledPeriods;
+ private SequenceableLoader sequenceableLoader;
+
+ public MergingMediaPeriod(MediaPeriod... periods) {
+ this.periods = periods;
+ streamPeriodIndices = new IdentityHashMap<>();
+ }
+
+ @Override
+ public void prepare(Callback callback) {
+ this.callback = callback;
+ pendingChildPrepareCount = periods.length;
+ for (MediaPeriod period : periods) {
+ period.prepare(this);
+ }
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ for (MediaPeriod period : periods) {
+ period.maybeThrowPrepareError();
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return trackGroups;
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ // Map each selection and stream onto a child period index.
+ int[] streamChildIndices = new int[selections.length];
+ int[] selectionChildIndices = new int[selections.length];
+ for (int i = 0; i < selections.length; i++) {
+ streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
+ : streamPeriodIndices.get(streams[i]);
+ selectionChildIndices[i] = C.INDEX_UNSET;
+ if (selections[i] != null) {
+ TrackGroup trackGroup = selections[i].getTrackGroup();
+ for (int j = 0; j < periods.length; j++) {
+ if (periods[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {
+ selectionChildIndices[i] = j;
+ break;
+ }
+ }
+ }
+ }
+ streamPeriodIndices.clear();
+ // Select tracks for each child, copying the resulting streams back into a new streams array.
+ SampleStream[] newStreams = new SampleStream[selections.length];
+ SampleStream[] childStreams = new SampleStream[selections.length];
+ TrackSelection[] childSelections = new TrackSelection[selections.length];
+ ArrayList<MediaPeriod> enabledPeriodsList = new ArrayList<>(periods.length);
+ for (int i = 0; i < periods.length; i++) {
+ for (int j = 0; j < selections.length; j++) {
+ childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
+ childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
+ }
+ long selectPositionUs = periods[i].selectTracks(childSelections, mayRetainStreamFlags,
+ childStreams, streamResetFlags, positionUs);
+ if (i == 0) {
+ positionUs = selectPositionUs;
+ } else if (selectPositionUs != positionUs) {
+ throw new IllegalStateException("Children enabled at different positions");
+ }
+ boolean periodEnabled = false;
+ for (int j = 0; j < selections.length; j++) {
+ if (selectionChildIndices[j] == i) {
+ // Assert that the child provided a stream for the selection.
+ Assertions.checkState(childStreams[j] != null);
+ newStreams[j] = childStreams[j];
+ periodEnabled = true;
+ streamPeriodIndices.put(childStreams[j], i);
+ } else if (streamChildIndices[j] == i) {
+ // Assert that the child cleared any previous stream.
+ Assertions.checkState(childStreams[j] == null);
+ }
+ }
+ if (periodEnabled) {
+ enabledPeriodsList.add(periods[i]);
+ }
+ }
+ // Copy the new streams back into the streams array.
+ System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
+ // Update the local state.
+ enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
+ enabledPeriodsList.toArray(enabledPeriods);
+ sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods);
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ for (MediaPeriod period : enabledPeriods) {
+ period.discardBuffer(positionUs);
+ }
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return sequenceableLoader.continueLoading(positionUs);
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return sequenceableLoader.getNextLoadPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ long positionUs = periods[0].readDiscontinuity();
+ // Periods other than the first one are not allowed to report discontinuities.
+ for (int i = 1; i < periods.length; i++) {
+ if (periods[i].readDiscontinuity() != C.TIME_UNSET) {
+ throw new IllegalStateException("Child reported discontinuity");
+ }
+ }
+ // It must be possible to seek enabled periods to the new position, if there is one.
+ if (positionUs != C.TIME_UNSET) {
+ for (MediaPeriod enabledPeriod : enabledPeriods) {
+ if (enabledPeriod != periods[0]
+ && enabledPeriod.seekToUs(positionUs) != positionUs) {
+ throw new IllegalStateException("Children seeked to different positions");
+ }
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ long bufferedPositionUs = Long.MAX_VALUE;
+ for (MediaPeriod period : enabledPeriods) {
+ long rendererBufferedPositionUs = period.getBufferedPositionUs();
+ if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+ bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+ }
+ }
+ return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ positionUs = enabledPeriods[0].seekToUs(positionUs);
+ // Additional periods must seek to the same position.
+ for (int i = 1; i < enabledPeriods.length; i++) {
+ if (enabledPeriods[i].seekToUs(positionUs) != positionUs) {
+ throw new IllegalStateException("Children seeked to different positions");
+ }
+ }
+ return positionUs;
+ }
+
+ // MediaPeriod.Callback implementation
+
+ @Override
+ public void onPrepared(MediaPeriod ignored) {
+ if (--pendingChildPrepareCount > 0) {
+ return;
+ }
+ int totalTrackGroupCount = 0;
+ for (MediaPeriod period : periods) {
+ totalTrackGroupCount += period.getTrackGroups().length;
+ }
+ TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
+ int trackGroupIndex = 0;
+ for (MediaPeriod period : periods) {
+ TrackGroupArray periodTrackGroups = period.getTrackGroups();
+ int periodTrackGroupCount = periodTrackGroups.length;
+ for (int j = 0; j < periodTrackGroupCount; j++) {
+ trackGroupArray[trackGroupIndex++] = periodTrackGroups.get(j);
+ }
+ }
+ trackGroups = new TrackGroupArray(trackGroupArray);
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod ignored) {
+ if (trackGroups == null) {
+ // Still preparing.
+ return;
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -0,0 +1,174 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Merges multiple {@link MediaSource}s.
+ * <p>
+ * The {@link Timeline}s of the sources being merged must have the same number of periods, and must
+ * not have any dynamic windows.
+ */
+public final class MergingMediaSource implements MediaSource {
+
+ /**
+ * Thrown when a {@link MergingMediaSource} cannot merge its sources.
+ */
+ public static final class IllegalMergeException extends IOException {
+
+ /**
+ * The reason the merge failed.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH})
+ public @interface Reason {}
+ /**
+ * The merge failed because one of the sources being merged has a dynamic window.
+ */
+ public static final int REASON_WINDOWS_ARE_DYNAMIC = 0;
+ /**
+ * The merge failed because the sources have different period counts.
+ */
+ public static final int REASON_PERIOD_COUNT_MISMATCH = 1;
+
+ /**
+ * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
+ * {@link #REASON_PERIOD_COUNT_MISMATCH}.
+ */
+ @Reason public final int reason;
+
+ /**
+ * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
+ * {@link #REASON_PERIOD_COUNT_MISMATCH}.
+ */
+ public IllegalMergeException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ }
+
+ private static final int PERIOD_COUNT_UNSET = -1;
+
+ private final MediaSource[] mediaSources;
+ private final ArrayList<MediaSource> pendingTimelineSources;
+ private final Timeline.Window window;
+
+ private Listener listener;
+ private Timeline primaryTimeline;
+ private Object primaryManifest;
+ private int periodCount;
+ private IllegalMergeException mergeError;
+
+ /**
+ * @param mediaSources The {@link MediaSource}s to merge.
+ */
+ public MergingMediaSource(MediaSource... mediaSources) {
+ this.mediaSources = mediaSources;
+ pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
+ window = new Timeline.Window();
+ periodCount = PERIOD_COUNT_UNSET;
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ this.listener = listener;
+ for (int i = 0; i < mediaSources.length; i++) {
+ final int sourceIndex = i;
+ mediaSources[sourceIndex].prepareSource(player, false, new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ handleSourceInfoRefreshed(sourceIndex, timeline, manifest);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ if (mergeError != null) {
+ throw mergeError;
+ }
+ for (MediaSource mediaSource : mediaSources) {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
+ for (int i = 0; i < periods.length; i++) {
+ periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs);
+ }
+ return new MergingMediaPeriod(periods);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod;
+ for (int i = 0; i < mediaSources.length; i++) {
+ mediaSources[i].releasePeriod(mergingPeriod.periods[i]);
+ }
+ }
+
+ @Override
+ public void releaseSource() {
+ for (MediaSource mediaSource : mediaSources) {
+ mediaSource.releaseSource();
+ }
+ }
+
+ private void handleSourceInfoRefreshed(int sourceIndex, Timeline timeline, Object manifest) {
+ if (mergeError == null) {
+ mergeError = checkTimelineMerges(timeline);
+ }
+ if (mergeError != null) {
+ return;
+ }
+ pendingTimelineSources.remove(mediaSources[sourceIndex]);
+ if (sourceIndex == 0) {
+ primaryTimeline = timeline;
+ primaryManifest = manifest;
+ }
+ if (pendingTimelineSources.isEmpty()) {
+ listener.onSourceInfoRefreshed(primaryTimeline, primaryManifest);
+ }
+ }
+
+ private IllegalMergeException checkTimelineMerges(Timeline timeline) {
+ int windowCount = timeline.getWindowCount();
+ for (int i = 0; i < windowCount; i++) {
+ if (timeline.getWindow(i, window, false).isDynamic) {
+ return new IllegalMergeException(IllegalMergeException.REASON_WINDOWS_ARE_DYNAMIC);
+ }
+ }
+ if (periodCount == PERIOD_COUNT_UNSET) {
+ periodCount = timeline.getPeriodCount();
+ } else if (timeline.getPeriodCount() != periodCount) {
+ return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);
+ }
+ return null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/SampleStream.java
@@ -0,0 +1,76 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import java.io.IOException;
+
+/**
+ * A stream of media samples (and associated format information).
+ */
+public interface SampleStream {
+
+ /**
+ * Returns whether data is available to be read.
+ * <p>
+ * Note: If the stream has ended then a buffer with the end of stream flag can always be read from
+ * {@link #readData(FormatHolder, DecoderInputBuffer, boolean)}. Hence an ended stream is always
+ * ready.
+ *
+ * @return Whether data is available to be read.
+ */
+ boolean isReady();
+
+ /**
+ * Throws an error that's preventing data from being read. Does nothing if no such error exists.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Attempts to read from the stream.
+ * <p>
+ * If the stream has ended then {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set on {@code buffer}
+ * and {@link C#RESULT_BUFFER_READ} is returned. Else if no data is available then
+ * {@link C#RESULT_NOTHING_READ} is returned. Else if the format of the media is changing or if
+ * {@code formatRequired} is set then {@code formatHolder} is populated and
+ * {@link C#RESULT_FORMAT_READ} is returned. Else {@code buffer} is populated and
+ * {@link C#RESULT_BUFFER_READ} is returned.
+ *
+ * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+ * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+ * end of the stream. If the end of the stream has been reached, the
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
+ * @param formatRequired Whether the caller requires that the format of the stream be read even if
+ * it's not changing. A sample will never be read if set to true, however it is still possible
+ * for the end of stream or nothing to be read.
+ * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+ * {@link C#RESULT_BUFFER_READ}.
+ */
+ int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired);
+
+ /**
+ * Attempts to skip to the keyframe before the specified position, or to the end of the stream if
+ * {@code positionUs} is beyond it.
+ *
+ * @param positionUs The specified time.
+ */
+ void skipData(long positionUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+
+// TODO: Clarify the requirements for implementing this interface [Internal ref: b/36250203].
+/**
+ * A loader that can proceed in approximate synchronization with other loaders.
+ */
+public interface SequenceableLoader {
+
+ /**
+ * A callback to be notified of {@link SequenceableLoader} events.
+ */
+ interface Callback<T extends SequenceableLoader> {
+
+ /**
+ * Called by the loader to indicate that it wishes for its {@link #continueLoading(long)} method
+ * to be called when it can continue to load data. Called on the playback thread.
+ */
+ void onContinueLoadingRequested(T source);
+
+ }
+
+ /**
+ * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
+ */
+ long getNextLoadPositionUs();
+
+ /**
+ * Attempts to continue loading.
+ *
+ * @param positionUs The current playback position.
+ * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
+ * a different value than prior to the call. False otherwise.
+ */
+ boolean continueLoading(long positionUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -0,0 +1,110 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link Timeline} consisting of a single period and static window.
+ */
+public final class SinglePeriodTimeline extends Timeline {
+
+ private static final Object ID = new Object();
+
+ private final long periodDurationUs;
+ private final long windowDurationUs;
+ private final long windowPositionInPeriodUs;
+ private final long windowDefaultStartPositionUs;
+ private final boolean isSeekable;
+ private final boolean isDynamic;
+
+ /**
+ * Creates a timeline of one period of known duration, and a static window starting at zero and
+ * extending to that duration.
+ *
+ * @param durationUs The duration of the period, in microseconds.
+ * @param isSeekable Whether seeking is supported within the period.
+ */
+ public SinglePeriodTimeline(long durationUs, boolean isSeekable) {
+ this(durationUs, durationUs, 0, 0, isSeekable, false);
+ }
+
+ /**
+ * Creates a timeline with one period of known duration, and a window of known duration starting
+ * at a specified position in the period.
+ *
+ * @param periodDurationUs The duration of the period in microseconds.
+ * @param windowDurationUs The duration of the window in microseconds.
+ * @param windowPositionInPeriodUs The position of the start of the window in the period, in
+ * microseconds.
+ * @param windowDefaultStartPositionUs The default position relative to the start of the window at
+ * which to begin playback, in microseconds.
+ * @param isSeekable Whether seeking is supported within the window.
+ * @param isDynamic Whether the window may change when the timeline is updated.
+ */
+ public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs,
+ long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable,
+ boolean isDynamic) {
+ this.periodDurationUs = periodDurationUs;
+ this.windowDurationUs = windowDurationUs;
+ this.windowPositionInPeriodUs = windowPositionInPeriodUs;
+ this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;
+ this.isSeekable = isSeekable;
+ this.isDynamic = isDynamic;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ Assertions.checkIndex(windowIndex, 0, 1);
+ Object id = setIds ? ID : null;
+ long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
+ if (isDynamic) {
+ windowDefaultStartPositionUs += defaultPositionProjectionUs;
+ if (windowDefaultStartPositionUs > windowDurationUs) {
+ // The projection takes us beyond the end of the live window.
+ windowDefaultStartPositionUs = C.TIME_UNSET;
+ }
+ }
+ return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic,
+ windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ Assertions.checkIndex(periodIndex, 0, 1);
+ Object id = setIds ? ID : null;
+ return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return ID.equals(uid) ? 0 : C.INDEX_UNSET;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -0,0 +1,294 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * A {@link MediaPeriod} with a single sample.
+ */
+/* package */ final class SingleSampleMediaPeriod implements MediaPeriod,
+ Loader.Callback<SingleSampleMediaPeriod.SourceLoadable> {
+
+ /**
+ * The initial size of the allocation used to hold the sample data.
+ */
+ private static final int INITIAL_SAMPLE_SIZE = 1024;
+
+ private final Uri uri;
+ private final DataSource.Factory dataSourceFactory;
+ private final int minLoadableRetryCount;
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final int eventSourceId;
+ private final TrackGroupArray tracks;
+ private final ArrayList<SampleStreamImpl> sampleStreams;
+ /* package */ final Loader loader;
+ /* package */ final Format format;
+
+ /* package */ boolean loadingFinished;
+ /* package */ byte[] sampleData;
+ /* package */ int sampleSize;
+
+ public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+ int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
+ int eventSourceId) {
+ this.uri = uri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.format = format;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.eventSourceId = eventSourceId;
+ tracks = new TrackGroupArray(new TrackGroup(format));
+ sampleStreams = new ArrayList<>();
+ loader = new Loader("Loader:SingleSampleMediaPeriod");
+ }
+
+ public void release() {
+ loader.release();
+ }
+
+ @Override
+ public void prepare(Callback callback) {
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ loader.maybeThrowError();
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return tracks;
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ sampleStreams.remove(streams[i]);
+ streams[i] = null;
+ }
+ if (streams[i] == null && selections[i] != null) {
+ SampleStreamImpl stream = new SampleStreamImpl();
+ sampleStreams.add(stream);
+ streams[i] = stream;
+ streamResetFlags[i] = true;
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading()) {
+ return false;
+ }
+ loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this,
+ minLoadableRetryCount);
+ return true;
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return loadingFinished || loader.isLoading() ? C.TIME_END_OF_SOURCE : 0;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ for (int i = 0; i < sampleStreams.size(); i++) {
+ sampleStreams.get(i).seekToUs(positionUs);
+ }
+ return positionUs;
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,
+ long loadDurationMs) {
+ sampleSize = loadable.sampleSize;
+ sampleData = loadable.sampleData;
+ loadingFinished = true;
+ }
+
+ @Override
+ public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ // Do nothing.
+ }
+
+ @Override
+ public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
+ IOException error) {
+ notifyLoadError(error);
+ return Loader.RETRY;
+ }
+
+ // Internal methods.
+
+ private void notifyLoadError(final IOException e) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onLoadError(eventSourceId, e);
+ }
+ });
+ }
+ }
+
+ private final class SampleStreamImpl implements SampleStream {
+
+ private static final int STREAM_STATE_SEND_FORMAT = 0;
+ private static final int STREAM_STATE_SEND_SAMPLE = 1;
+ private static final int STREAM_STATE_END_OF_STREAM = 2;
+
+ private int streamState;
+
+ public void seekToUs(long positionUs) {
+ if (streamState == STREAM_STATE_END_OF_STREAM) {
+ streamState = STREAM_STATE_SEND_SAMPLE;
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ return loadingFinished;
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ if (streamState == STREAM_STATE_END_OF_STREAM) {
+ buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
+ } else if (requireFormat || streamState == STREAM_STATE_SEND_FORMAT) {
+ formatHolder.format = format;
+ streamState = STREAM_STATE_SEND_SAMPLE;
+ return C.RESULT_FORMAT_READ;
+ }
+
+ Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE);
+ if (!loadingFinished) {
+ return C.RESULT_NOTHING_READ;
+ } else {
+ buffer.timeUs = 0;
+ buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
+ buffer.ensureSpaceForWrite(sampleSize);
+ buffer.data.put(sampleData, 0, sampleSize);
+ streamState = STREAM_STATE_END_OF_STREAM;
+ return C.RESULT_BUFFER_READ;
+ }
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ if (positionUs > 0) {
+ streamState = STREAM_STATE_END_OF_STREAM;
+ }
+ }
+
+ }
+
+ /* package */ static final class SourceLoadable implements Loadable {
+
+ private final Uri uri;
+ private final DataSource dataSource;
+
+ private int sampleSize;
+ private byte[] sampleData;
+
+ public SourceLoadable(Uri uri, DataSource dataSource) {
+ this.uri = uri;
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ public void cancelLoad() {
+ // Never happens.
+ }
+
+ @Override
+ public boolean isLoadCanceled() {
+ return false;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ // We always load from the beginning, so reset the sampleSize to 0.
+ sampleSize = 0;
+ try {
+ // Create and open the input.
+ dataSource.open(new DataSpec(uri));
+ // Load the sample data.
+ int result = 0;
+ while (result != C.RESULT_END_OF_INPUT) {
+ sampleSize += result;
+ if (sampleData == null) {
+ sampleData = new byte[INITIAL_SAMPLE_SIZE];
+ } else if (sampleSize == sampleData.length) {
+ sampleData = Arrays.copyOf(sampleData, sampleData.length * 2);
+ }
+ result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -0,0 +1,114 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}.
+ */
+public final class SingleSampleMediaSource implements MediaSource {
+
+ /**
+ * Listener of {@link SingleSampleMediaSource} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Called when an error occurs loading media data.
+ *
+ * @param sourceId The id of the reporting {@link SingleSampleMediaSource}.
+ * @param e The cause of the failure.
+ */
+ void onLoadError(int sourceId, IOException e);
+
+ }
+
+ /**
+ * The default minimum number of times to retry loading data prior to failing.
+ */
+ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+
+ private final Uri uri;
+ private final DataSource.Factory dataSourceFactory;
+ private final Format format;
+ private final int minLoadableRetryCount;
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final int eventSourceId;
+ private final Timeline timeline;
+
+ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+ long durationUs) {
+ this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
+ }
+
+ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+ long durationUs, int minLoadableRetryCount) {
+ this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0);
+ }
+
+ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+ long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
+ int eventSourceId) {
+ this.uri = uri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.format = format;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.eventSourceId = eventSourceId;
+ timeline = new SinglePeriodTimeline(durationUs, true);
+ }
+
+ // MediaSource implementation.
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ listener.onSourceInfoRefreshed(timeline, null);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ Assertions.checkArgument(index == 0);
+ return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
+ eventHandler, eventListener, eventSourceId);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((SingleSampleMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ public void releaseSource() {
+ // Do nothing.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/TrackGroup.java
@@ -0,0 +1,100 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+// TODO: Add an allowMultipleStreams boolean to indicate where the one stream per group restriction
+// does not apply.
+/**
+ * Defines a group of tracks exposed by a {@link MediaPeriod}.
+ * <p>
+ * A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a group
+ * at any given time, however this {@link SampleStream} may adapt between multiple tracks within the
+ * group.
+ */
+public final class TrackGroup {
+
+ /**
+ * The number of tracks in the group.
+ */
+ public final int length;
+
+ private final Format[] formats;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param formats The track formats. Must not be null or contain null elements.
+ */
+ public TrackGroup(Format... formats) {
+ Assertions.checkState(formats.length > 0);
+ this.formats = formats;
+ this.length = formats.length;
+ }
+
+ /**
+ * Returns the format of the track at a given index.
+ *
+ * @param index The index of the track.
+ * @return The track's format.
+ */
+ public Format getFormat(int index) {
+ return formats[index];
+ }
+
+ /**
+ * Returns the index of the track with the given format in the group.
+ *
+ * @param format The format.
+ * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
+ */
+ public int indexOf(Format format) {
+ for (int i = 0; i < formats.length; i++) {
+ if (format == formats[i]) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + Arrays.hashCode(formats);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackGroup other = (TrackGroup) obj;
+ return length == other.length && Arrays.equals(formats, other.formats);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/TrackGroupArray.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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import java.util.Arrays;
+
+/**
+ * An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}.
+ */
+public final class TrackGroupArray {
+
+ /**
+ * The empty array.
+ */
+ public static final TrackGroupArray EMPTY = new TrackGroupArray();
+
+ /**
+ * The number of groups in the array. Greater than or equal to zero.
+ */
+ public final int length;
+
+ private final TrackGroup[] trackGroups;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param trackGroups The groups. Must not be null or contain null elements, but may be empty.
+ */
+ public TrackGroupArray(TrackGroup... trackGroups) {
+ this.trackGroups = trackGroups;
+ this.length = trackGroups.length;
+ }
+
+ /**
+ * Returns the group at a given index.
+ *
+ * @param index The index of the group.
+ * @return The group.
+ */
+ public TrackGroup get(int index) {
+ return trackGroups[index];
+ }
+
+ /**
+ * Returns the index of a group within the array.
+ *
+ * @param group The group.
+ * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
+ */
+ public int indexOf(TrackGroup group) {
+ for (int i = 0; i < length; i++) {
+ if (trackGroups[i] == group) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ hashCode = Arrays.hashCode(trackGroups);
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackGroupArray other = (TrackGroupArray) obj;
+ return length == other.length && Arrays.equals(trackGroups, other.trackGroups);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.ParserException;
+
+/**
+ * Thrown if the input format was not recognized.
+ */
+public class UnrecognizedInputFormatException extends ParserException {
+
+ /**
+ * The {@link Uri} from which the unrecognized data was read.
+ */
+ public final Uri uri;
+
+ /**
+ * @param message The detail message for the exception.
+ * @param uri The {@link Uri} from which the unrecognized data was read.
+ */
+ public UnrecognizedInputFormatException(String message, Uri uri) {
+ super(message);
+ this.uri = uri;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+
+/**
+ * A base implementation of {@link MediaChunk} that outputs to a {@link BaseMediaChunkOutput}.
+ */
+public abstract class BaseMediaChunk extends MediaChunk {
+
+ private BaseMediaChunkOutput output;
+ private int[] firstSampleIndices;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param chunkIndex The index of the chunk.
+ */
+ public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+ int chunkIndex) {
+ super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+ endTimeUs, chunkIndex);
+ }
+
+ /**
+ * Initializes the chunk for loading, setting the {@link BaseMediaChunkOutput} that will receive
+ * samples as they are loaded.
+ *
+ * @param output The output that will receive the loaded media samples.
+ */
+ public void init(BaseMediaChunkOutput output) {
+ this.output = output;
+ firstSampleIndices = output.getWriteIndices();
+ }
+
+ /**
+ * Returns the index of the first sample in the specified track of the output that will originate
+ * from this chunk.
+ */
+ public final int getFirstSampleIndex(int trackIndex) {
+ return firstSampleIndices[trackIndex];
+ }
+
+ /**
+ * Returns the output most recently passed to {@link #init(BaseMediaChunkOutput)}.
+ */
+ protected final BaseMediaChunkOutput getOutput() {
+ return output;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java
@@ -0,0 +1,79 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import android.util.Log;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
+
+/**
+ * An output for {@link BaseMediaChunk}s.
+ */
+/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider {
+
+ private static final String TAG = "BaseMediaChunkOutput";
+
+ private final int[] trackTypes;
+ private final DefaultTrackOutput[] trackOutputs;
+
+ /**
+ * @param trackTypes The track types of the individual track outputs.
+ * @param trackOutputs The individual track outputs.
+ */
+ public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) {
+ this.trackTypes = trackTypes;
+ this.trackOutputs = trackOutputs;
+ }
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ for (int i = 0; i < trackTypes.length; i++) {
+ if (type == trackTypes[i]) {
+ return trackOutputs[i];
+ }
+ }
+ Log.e(TAG, "Unmatched track of type: " + type);
+ return new DummyTrackOutput();
+ }
+
+ /**
+ * Returns the current absolute write indices of the individual track outputs.
+ */
+ public int[] getWriteIndices() {
+ int[] writeIndices = new int[trackOutputs.length];
+ for (int i = 0; i < trackOutputs.length; i++) {
+ if (trackOutputs[i] != null) {
+ writeIndices[i] = trackOutputs[i].getWriteIndex();
+ }
+ }
+ return writeIndices;
+ }
+
+ /**
+ * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
+ * subsequently written to the track outputs.
+ */
+ public void setSampleOffsetUs(long sampleOffsetUs) {
+ for (DefaultTrackOutput trackOutput : trackOutputs) {
+ if (trackOutput != null) {
+ trackOutput.setSampleOffsetUs(sampleOffsetUs);
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/Chunk.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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * An abstract base class for {@link Loadable} implementations that load chunks of data required
+ * for the playback of streams.
+ */
+public abstract class Chunk implements Loadable {
+
+ /**
+ * The {@link DataSpec} that defines the data to be loaded.
+ */
+ public final DataSpec dataSpec;
+ /**
+ * The type of the chunk. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For
+ * reporting only.
+ */
+ public final int type;
+ /**
+ * The format of the track to which this chunk belongs, or null if the chunk does not belong to
+ * a track.
+ */
+ public final Format trackFormat;
+ /**
+ * One of the {@link C} {@code SELECTION_REASON_*} constants if the chunk belongs to a track.
+ * {@link C#SELECTION_REASON_UNKNOWN} if the chunk does not belong to a track.
+ */
+ public final int trackSelectionReason;
+ /**
+ * Optional data associated with the selection of the track to which this chunk belongs. Null if
+ * the chunk does not belong to a track.
+ */
+ public final Object trackSelectionData;
+ /**
+ * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data
+ * being loaded does not contain media samples.
+ */
+ public final long startTimeUs;
+ /**
+ * The end time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data being
+ * loaded does not contain media samples.
+ */
+ public final long endTimeUs;
+
+ protected final DataSource dataSource;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param type See {@link #type}.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs See {@link #startTimeUs}.
+ * @param endTimeUs See {@link #endTimeUs}.
+ */
+ public Chunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs) {
+ this.dataSource = Assertions.checkNotNull(dataSource);
+ this.dataSpec = Assertions.checkNotNull(dataSpec);
+ this.type = type;
+ this.trackFormat = trackFormat;
+ this.trackSelectionReason = trackSelectionReason;
+ this.trackSelectionData = trackSelectionData;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ }
+
+ /**
+ * Returns the duration of the chunk in microseconds.
+ */
+ public final long getDurationUs() {
+ return endTimeUs - startTimeUs;
+ }
+
+ /**
+ * Returns the number of bytes that have been loaded.
+ */
+ public abstract long bytesLoaded();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -0,0 +1,195 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * An {@link Extractor} wrapper for loading chunks containing a single track.
+ * <p>
+ * The wrapper allows switching of the {@link TrackOutput} that receives parsed data.
+ */
+public final class ChunkExtractorWrapper implements ExtractorOutput {
+
+ /**
+ * Provides {@link TrackOutput} instances to be written to by the wrapper.
+ */
+ public interface TrackOutputProvider {
+
+ /**
+ * Called to get the {@link TrackOutput} for a specific track.
+ * <p>
+ * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
+ *
+ * @param id A track identifier.
+ * @param type The type of the track. Typically one of the
+ * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants.
+ * @return The {@link TrackOutput} for the given track identifier.
+ */
+ TrackOutput track(int id, int type);
+
+ }
+
+ public final Extractor extractor;
+
+ private final Format manifestFormat;
+ private final SparseArray<BindingTrackOutput> bindingTrackOutputs;
+
+ private boolean extractorInitialized;
+ private TrackOutputProvider trackOutputProvider;
+ private SeekMap seekMap;
+ private Format[] sampleFormats;
+
+ /**
+ * @param extractor The extractor to wrap.
+ * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any
+ * sample {@link Format} output from the {@link Extractor}.
+ */
+ public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) {
+ this.extractor = extractor;
+ this.manifestFormat = manifestFormat;
+ bindingTrackOutputs = new SparseArray<>();
+ }
+
+ /**
+ * Returns the {@link SeekMap} most recently output by the extractor, or null.
+ */
+ public SeekMap getSeekMap() {
+ return seekMap;
+ }
+
+ /**
+ * Returns the sample {@link Format}s most recently output by the extractor, or null.
+ */
+ public Format[] getSampleFormats() {
+ return sampleFormats;
+ }
+
+ /**
+ * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to
+ * receive data from a new chunk.
+ *
+ * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data.
+ */
+ public void init(TrackOutputProvider trackOutputProvider) {
+ this.trackOutputProvider = trackOutputProvider;
+ if (!extractorInitialized) {
+ extractor.init(this);
+ extractorInitialized = true;
+ } else {
+ extractor.seek(0, 0);
+ for (int i = 0; i < bindingTrackOutputs.size(); i++) {
+ bindingTrackOutputs.valueAt(i).bind(trackOutputProvider);
+ }
+ }
+ }
+
+ // ExtractorOutput implementation.
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ BindingTrackOutput bindingTrackOutput = bindingTrackOutputs.get(id);
+ if (bindingTrackOutput == null) {
+ // Assert that if we're seeing a new track we have not seen endTracks.
+ Assertions.checkState(sampleFormats == null);
+ bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat);
+ bindingTrackOutput.bind(trackOutputProvider);
+ bindingTrackOutputs.put(id, bindingTrackOutput);
+ }
+ return bindingTrackOutput;
+ }
+
+ @Override
+ public void endTracks() {
+ Format[] sampleFormats = new Format[bindingTrackOutputs.size()];
+ for (int i = 0; i < bindingTrackOutputs.size(); i++) {
+ sampleFormats[i] = bindingTrackOutputs.valueAt(i).sampleFormat;
+ }
+ this.sampleFormats = sampleFormats;
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ this.seekMap = seekMap;
+ }
+
+ // Internal logic.
+
+ private static final class BindingTrackOutput implements TrackOutput {
+
+ private final int id;
+ private final int type;
+ private final Format manifestFormat;
+
+ public Format sampleFormat;
+ private TrackOutput trackOutput;
+
+ public BindingTrackOutput(int id, int type, Format manifestFormat) {
+ this.id = id;
+ this.type = type;
+ this.manifestFormat = manifestFormat;
+ }
+
+ public void bind(TrackOutputProvider trackOutputProvider) {
+ if (trackOutputProvider == null) {
+ trackOutput = new DummyTrackOutput();
+ return;
+ }
+ trackOutput = trackOutputProvider.track(id, type);
+ if (trackOutput != null) {
+ trackOutput.format(sampleFormat);
+ }
+ }
+
+ @Override
+ public void format(Format format) {
+ // TODO: This should only happen for the primary track. Additional metadata/text tracks need
+ // to be copied with different manifest derived formats.
+ sampleFormat = format.copyWithManifestFormatInfo(manifestFormat);
+ trackOutput.format(sampleFormat);
+ }
+
+ @Override
+ public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+ throws IOException, InterruptedException {
+ return trackOutput.sampleData(input, length, allowEndOfInput);
+ }
+
+ @Override
+ public void sampleData(ParsableByteArray data, int length) {
+ trackOutput.sampleData(data, length);
+ }
+
+ @Override
+ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+ byte[] encryptionKey) {
+ trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java
@@ -0,0 +1,41 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+/**
+ * Holds a chunk or an indication that the end of the stream has been reached.
+ */
+public final class ChunkHolder {
+
+ /**
+ * The chunk.
+ */
+ public Chunk chunk;
+
+ /**
+ * Indicates that the end of the stream has been reached.
+ */
+ public boolean endOfStream;
+
+ /**
+ * Clears the holder.
+ */
+ public void clear() {
+ chunk = null;
+ endOfStream = false;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -0,0 +1,485 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.SequenceableLoader;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}.
+ * May also be configured to expose additional embedded {@link SampleStream}s.
+ */
+public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
+ Loader.Callback<Chunk> {
+
+ private final int primaryTrackType;
+ private final int[] embeddedTrackTypes;
+ private final boolean[] embeddedTracksSelected;
+ private final T chunkSource;
+ private final SequenceableLoader.Callback<ChunkSampleStream<T>> callback;
+ private final EventDispatcher eventDispatcher;
+ private final int minLoadableRetryCount;
+ private final Loader loader;
+ private final ChunkHolder nextChunkHolder;
+ private final LinkedList<BaseMediaChunk> mediaChunks;
+ private final List<BaseMediaChunk> readOnlyMediaChunks;
+ private final DefaultTrackOutput primarySampleQueue;
+ private final DefaultTrackOutput[] embeddedSampleQueues;
+ private final BaseMediaChunkOutput mediaChunkOutput;
+
+ private Format primaryDownstreamTrackFormat;
+ private long pendingResetPositionUs;
+ /* package */ long lastSeekPositionUs;
+ /* package */ boolean loadingFinished;
+
+ /**
+ * @param primaryTrackType The type of the primary track. One of the {@link C}
+ * {@code TRACK_TYPE_*} constants.
+ * @param embeddedTrackTypes The types of any embedded tracks, or null.
+ * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
+ * @param callback An {@link Callback} for the stream.
+ * @param allocator An {@link Allocator} from which allocations can be obtained.
+ * @param positionUs The position from which to start loading media.
+ * @param minLoadableRetryCount The minimum number of times that the source should retry a load
+ * before propagating an error.
+ * @param eventDispatcher A dispatcher to notify of events.
+ */
+ public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource,
+ Callback<ChunkSampleStream<T>> callback, Allocator allocator, long positionUs,
+ int minLoadableRetryCount, EventDispatcher eventDispatcher) {
+ this.primaryTrackType = primaryTrackType;
+ this.embeddedTrackTypes = embeddedTrackTypes;
+ this.chunkSource = chunkSource;
+ this.callback = callback;
+ this.eventDispatcher = eventDispatcher;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ loader = new Loader("Loader:ChunkSampleStream");
+ nextChunkHolder = new ChunkHolder();
+ mediaChunks = new LinkedList<>();
+ readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+
+ int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;
+ embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount];
+ embeddedTracksSelected = new boolean[embeddedTrackCount];
+ int[] trackTypes = new int[1 + embeddedTrackCount];
+ DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount];
+
+ primarySampleQueue = new DefaultTrackOutput(allocator);
+ trackTypes[0] = primaryTrackType;
+ sampleQueues[0] = primarySampleQueue;
+
+ for (int i = 0; i < embeddedTrackCount; i++) {
+ DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator);
+ embeddedSampleQueues[i] = trackOutput;
+ sampleQueues[i + 1] = trackOutput;
+ trackTypes[i + 1] = embeddedTrackTypes[i];
+ }
+
+ mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues);
+ pendingResetPositionUs = positionUs;
+ lastSeekPositionUs = positionUs;
+ }
+
+ /**
+ * Discards buffered media for embedded tracks that are not currently selected, up to the
+ * specified position.
+ *
+ * @param positionUs The position to discard up to, in microseconds.
+ */
+ public void discardUnselectedEmbeddedTracksTo(long positionUs) {
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ if (!embeddedTracksSelected[i]) {
+ embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
+ }
+ }
+ }
+
+ /**
+ * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's
+ * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned
+ * stream when the track is no longer required, and before calling this method again to obtain
+ * another stream for the same track.
+ *
+ * @param positionUs The current playback position in microseconds.
+ * @param trackType The type of the embedded track to enable.
+ * @return The {@link EmbeddedSampleStream} for the embedded track.
+ */
+ public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) {
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ if (embeddedTrackTypes[i] == trackType) {
+ Assertions.checkState(!embeddedTracksSelected[i]);
+ embeddedTracksSelected[i] = true;
+ embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
+ return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i);
+ }
+ }
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Returns the {@link ChunkSource} used by this stream.
+ */
+ public T getChunkSource() {
+ return chunkSource;
+ }
+
+ /**
+ * Returns an estimate of the position up to which data is buffered.
+ *
+ * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+ * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
+ */
+ public long getBufferedPositionUs() {
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ long bufferedPositionUs = lastSeekPositionUs;
+ BaseMediaChunk lastMediaChunk = mediaChunks.getLast();
+ BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
+ : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
+ if (lastCompletedMediaChunk != null) {
+ bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
+ }
+ return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs());
+ }
+ }
+
+ /**
+ * Seeks to the specified position in microseconds.
+ *
+ * @param positionUs The seek position in microseconds.
+ */
+ public void seekToUs(long positionUs) {
+ lastSeekPositionUs = positionUs;
+ // If we're not pending a reset, see if we can seek within the primary sample queue.
+ boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore(
+ positionUs, positionUs < getNextLoadPositionUs());
+ if (seekInsideBuffer) {
+ // We succeeded. We need to discard any chunks that we've moved past and perform the seek for
+ // any embedded streams as well.
+ while (mediaChunks.size() > 1
+ && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) {
+ mediaChunks.removeFirst();
+ }
+ // TODO: For this to work correctly, the embedded streams must not discard anything from their
+ // sample queues beyond the current read position of the primary stream.
+ for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.skipToKeyframeBefore(positionUs, true);
+ }
+ } else {
+ // We failed, and need to restart.
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ mediaChunks.clear();
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ primarySampleQueue.reset(true);
+ for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Releases the stream.
+ * <p>
+ * This method should be called when the stream is no longer required.
+ */
+ public void release() {
+ primarySampleQueue.disable();
+ for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.disable();
+ }
+ loader.release();
+ }
+
+ // SampleStream implementation.
+
+ @Override
+ public boolean isReady() {
+ return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty());
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ if (!loader.isLoading()) {
+ chunkSource.maybeThrowError();
+ }
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
+ return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ lastSeekPositionUs);
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
+ primarySampleQueue.skipAll();
+ } else {
+ primarySampleQueue.skipToKeyframeBefore(positionUs, true);
+ }
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ chunkSource.onChunkLoadCompleted(loadable);
+ eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, primaryTrackType,
+ loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData,
+ loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs,
+ loadable.bytesLoaded());
+ callback.onContinueLoadingRequested(this);
+ }
+
+ @Override
+ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, primaryTrackType,
+ loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData,
+ loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs,
+ loadable.bytesLoaded());
+ if (!released) {
+ primarySampleQueue.reset(true);
+ for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.reset(true);
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ IOException error) {
+ long bytesLoaded = loadable.bytesLoaded();
+ boolean isMediaChunk = isMediaChunk(loadable);
+ boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1;
+ boolean canceled = false;
+ if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
+ canceled = true;
+ if (isMediaChunk) {
+ BaseMediaChunk removed = mediaChunks.removeLast();
+ Assertions.checkState(removed == loadable);
+ primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
+ }
+ if (mediaChunks.isEmpty()) {
+ pendingResetPositionUs = lastSeekPositionUs;
+ }
+ }
+ }
+ eventDispatcher.loadError(loadable.dataSpec, loadable.type, primaryTrackType,
+ loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData,
+ loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded,
+ error, canceled);
+ if (canceled) {
+ callback.onContinueLoadingRequested(this);
+ return Loader.DONT_RETRY;
+ } else {
+ return Loader.RETRY;
+ }
+ }
+
+ // SequenceableLoader implementation
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading()) {
+ return false;
+ }
+
+ chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(),
+ pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs,
+ nextChunkHolder);
+ boolean endOfStream = nextChunkHolder.endOfStream;
+ Chunk loadable = nextChunkHolder.chunk;
+ nextChunkHolder.clear();
+
+ if (endOfStream) {
+ loadingFinished = true;
+ return true;
+ }
+
+ if (loadable == null) {
+ return false;
+ }
+
+ if (isMediaChunk(loadable)) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;
+ mediaChunk.init(mediaChunkOutput);
+ mediaChunks.add(mediaChunk);
+ }
+ long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount);
+ eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, primaryTrackType,
+ loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData,
+ loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs);
+ return true;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs;
+ }
+ }
+
+ // Internal methods
+
+ // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming.
+ /**
+ * Discards media chunks from the back of the buffer if conditions have changed such that it's
+ * preferable to re-buffer the media at a different quality.
+ *
+ * @param positionUs The current playback position in microseconds.
+ */
+ private void maybeDiscardUpstream(long positionUs) {
+ int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
+ discardUpstreamMediaChunks(Math.max(1, queueSize));
+ }
+
+ private boolean isMediaChunk(Chunk chunk) {
+ return chunk instanceof BaseMediaChunk;
+ }
+
+ /* package */ boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ private void discardDownstreamMediaChunks(int primaryStreamReadIndex) {
+ while (mediaChunks.size() > 1
+ && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) {
+ mediaChunks.removeFirst();
+ }
+ BaseMediaChunk currentChunk = mediaChunks.getFirst();
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ primaryDownstreamTrackFormat = trackFormat;
+ }
+
+ /**
+ * Discard upstream media chunks until the queue length is equal to the length specified.
+ *
+ * @param queueLength The desired length of the queue.
+ * @return Whether chunks were discarded.
+ */
+ private boolean discardUpstreamMediaChunks(int queueLength) {
+ if (mediaChunks.size() <= queueLength) {
+ return false;
+ }
+ long startTimeUs = 0;
+ long endTimeUs = mediaChunks.getLast().endTimeUs;
+ BaseMediaChunk removed = null;
+ while (mediaChunks.size() > queueLength) {
+ removed = mediaChunks.removeLast();
+ startTimeUs = removed.startTimeUs;
+ loadingFinished = false;
+ }
+ primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
+ }
+ eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
+ return true;
+ }
+
+ /**
+ * A {@link SampleStream} embedded in a {@link ChunkSampleStream}.
+ */
+ public final class EmbeddedSampleStream implements SampleStream {
+
+ public final ChunkSampleStream<T> parent;
+
+ private final DefaultTrackOutput sampleQueue;
+ private final int index;
+
+ public EmbeddedSampleStream(ChunkSampleStream<T> parent, DefaultTrackOutput sampleQueue,
+ int index) {
+ this.parent = parent;
+ this.sampleQueue = sampleQueue;
+ this.index = index;
+ }
+
+ @Override
+ public boolean isReady() {
+ return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty());
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
+ sampleQueue.skipAll();
+ } else {
+ sampleQueue.skipToKeyframeBefore(positionUs, true);
+ }
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing. Errors will be thrown from the primary stream.
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean formatRequired) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
+ lastSeekPositionUs);
+ }
+
+ public void release() {
+ Assertions.checkState(embeddedTracksSelected[index]);
+ embeddedTracksSelected[index] = false;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A provider of {@link Chunk}s for a {@link ChunkSampleStream} to load.
+ */
+public interface ChunkSource {
+
+ /**
+ * If the source is currently having difficulty providing chunks, then this method throws the
+ * underlying error. Otherwise does nothing.
+ * <p>
+ * This method should only be called after the source has been prepared.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
+ * <p>
+ * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced
+ * with chunks of a significantly higher quality (e.g. because the available bandwidth has
+ * substantially increased).
+ *
+ * @param playbackPositionUs The current playback position.
+ * @param queue The queue of buffered {@link MediaChunk}s.
+ * @return The preferred queue size.
+ */
+ int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
+
+ /**
+ * Returns the next chunk to load.
+ * <p>
+ * If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has
+ * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the
+ * end of the stream has not been reached, the {@link ChunkHolder} is not modified.
+ *
+ * @param previous The most recently loaded media chunk.
+ * @param playbackPositionUs The current playback position. If {@code previous} is null then this
+ * parameter is the position from which playback is expected to start (or restart) and hence
+ * should be interpreted as a seek position.
+ * @param out A holder to populate.
+ */
+ void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out);
+
+ /**
+ * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this
+ * source.
+ * <p>
+ * This method should only be called when the source is enabled.
+ *
+ * @param chunk The chunk whose load has been completed.
+ */
+ void onChunkLoadCompleted(Chunk chunk);
+
+ /**
+ * Called when the {@link ChunkSampleStream} encounters an error loading a chunk obtained from
+ * this source.
+ * <p>
+ * This method should only be called when the source is enabled.
+ *
+ * @param chunk The chunk whose load encountered the error.
+ * @param cancelable Whether the load can be canceled.
+ * @param e The error.
+ * @return Whether the load should be canceled.
+ */
+ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import android.util.Log;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
+
+/**
+ * Helper class for blacklisting tracks in a {@link TrackSelection} when 404 (Not Found) and 410
+ * (Gone) HTTP response codes are encountered.
+ */
+public final class ChunkedTrackBlacklistUtil {
+
+ /**
+ * The default duration for which a track is blacklisted in milliseconds.
+ */
+ public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000;
+
+ private static final String TAG = "ChunkedTrackBlacklist";
+
+ /**
+ * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for
+ * {@link #DEFAULT_TRACK_BLACKLIST_MS} if {@code e} is an {@link InvalidResponseCodeException}
+ * with {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing.
+ * Note that blacklisting will fail if the track is the only non-blacklisted track in the
+ * selection.
+ *
+ * @param trackSelection The track selection.
+ * @param trackSelectionIndex The index in the selection to consider blacklisting.
+ * @param e The error to inspect.
+ * @return Whether the track was blacklisted in the selection.
+ */
+ public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex,
+ Exception e) {
+ return maybeBlacklistTrack(trackSelection, trackSelectionIndex, e, DEFAULT_TRACK_BLACKLIST_MS);
+ }
+
+ /**
+ * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for
+ * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e}
+ * returns true. Else does nothing. Note that blacklisting will fail if the track is the only
+ * non-blacklisted track in the selection.
+ *
+ * @param trackSelection The track selection.
+ * @param trackSelectionIndex The index in the selection to consider blacklisting.
+ * @param e The error to inspect.
+ * @param blacklistDurationMs The duration to blacklist the track for, if it is blacklisted.
+ * @return Whether the track was blacklisted.
+ */
+ public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex,
+ Exception e, long blacklistDurationMs) {
+ if (shouldBlacklist(e)) {
+ boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
+ int responseCode = ((InvalidResponseCodeException) e).responseCode;
+ if (blacklisted) {
+ Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode="
+ + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
+ } else {
+ Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode="
+ + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
+ }
+ return blacklisted;
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether a loading error is an {@link InvalidResponseCodeException} with
+ * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410.
+ *
+ * @param e The loading error.
+ * @return Wheter the loading error is an {@link InvalidResponseCodeException} with
+ * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410.
+ */
+ public static boolean shouldBlacklist(Exception e) {
+ if (e instanceof InvalidResponseCodeException) {
+ int responseCode = ((InvalidResponseCodeException) e).responseCode;
+ return responseCode == 404 || responseCode == 410;
+ }
+ return false;
+ }
+
+ private ChunkedTrackBlacklistUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -0,0 +1,124 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data.
+ */
+public class ContainerMediaChunk extends BaseMediaChunk {
+
+ private final int chunkCount;
+ private final long sampleOffsetUs;
+ private final ChunkExtractorWrapper extractorWrapper;
+
+ private volatile int bytesLoaded;
+ private volatile boolean loadCanceled;
+ private volatile boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param chunkIndex The index of the chunk.
+ * @param chunkCount The number of chunks in the underlying media that are spanned by this
+ * instance. Normally equal to one, but may be larger if multiple chunks as defined by the
+ * underlying media are being merged into a single load.
+ * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.
+ * @param extractorWrapper A wrapped extractor to use for parsing the data.
+ */
+ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+ int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) {
+ super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+ endTimeUs, chunkIndex);
+ this.chunkCount = chunkCount;
+ this.sampleOffsetUs = sampleOffsetUs;
+ this.extractorWrapper = extractorWrapper;
+ }
+
+ @Override
+ public int getNextChunkIndex() {
+ return chunkIndex + chunkCount;
+ }
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ @Override
+ public final long bytesLoaded() {
+ return bytesLoaded;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public final void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public final boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ try {
+ // Create and open the input.
+ ExtractorInput input = new DefaultExtractorInput(dataSource,
+ loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+ if (bytesLoaded == 0) {
+ // Configure the output and set it as the target for the extractor wrapper.
+ BaseMediaChunkOutput output = getOutput();
+ output.setSampleOffsetUs(sampleOffsetUs);
+ extractorWrapper.init(output);
+ }
+ // Load and decode the sample data.
+ try {
+ Extractor extractor = extractorWrapper.extractor;
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, null);
+ }
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
+ } finally {
+ bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ loadCompleted = true;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/DataChunk.java
@@ -0,0 +1,124 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A base class for {@link Chunk} implementations where the data should be loaded into a
+ * {@code byte[]} before being consumed.
+ */
+public abstract class DataChunk extends Chunk {
+
+ private static final int READ_GRANULARITY = 16 * 1024;
+
+ private byte[] data;
+ private int limit;
+
+ private volatile boolean loadCanceled;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param type See {@link #type}.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param data An optional recycled array that can be used as a holder for the data.
+ */
+ public DataChunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, byte[] data) {
+ super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData,
+ C.TIME_UNSET, C.TIME_UNSET);
+ this.data = data;
+ }
+
+ /**
+ * Returns the array in which the data is held.
+ * <p>
+ * This method should be used for recycling the holder only, and not for reading the data.
+ *
+ * @return The array in which the data is held.
+ */
+ public byte[] getDataHolder() {
+ return data;
+ }
+
+ @Override
+ public long bytesLoaded() {
+ return limit;
+ }
+
+ // Loadable implementation
+
+ @Override
+ public final void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public final boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ try {
+ dataSource.open(dataSpec);
+ limit = 0;
+ int bytesRead = 0;
+ while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) {
+ maybeExpandData();
+ bytesRead = dataSource.read(data, limit, READ_GRANULARITY);
+ if (bytesRead != -1) {
+ limit += bytesRead;
+ }
+ }
+ if (!loadCanceled) {
+ consume(data, limit);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ /**
+ * Called by {@link #load()}. Implementations should override this method to consume the loaded
+ * data.
+ *
+ * @param data An array containing the data.
+ * @param limit The limit of the data.
+ * @throws IOException If an error occurs consuming the loaded data.
+ */
+ protected abstract void consume(byte[] data, int limit) throws IOException;
+
+ private void maybeExpandData() {
+ if (data == null) {
+ data = new byte[READ_GRANULARITY];
+ } else if (data.length < limit + READ_GRANULARITY) {
+ // The new length is calculated as (data.length + READ_GRANULARITY) rather than
+ // (limit + READ_GRANULARITY) in order to avoid small increments in the length.
+ data = Arrays.copyOf(data, data.length + READ_GRANULARITY);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track.
+ */
+public final class InitializationChunk extends Chunk {
+
+ private final ChunkExtractorWrapper extractorWrapper;
+
+ private volatile int bytesLoaded;
+ private volatile boolean loadCanceled;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param extractorWrapper A wrapped extractor to use for parsing the initialization data.
+ */
+ public InitializationChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData,
+ ChunkExtractorWrapper extractorWrapper) {
+ super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason,
+ trackSelectionData, C.TIME_UNSET, C.TIME_UNSET);
+ this.extractorWrapper = extractorWrapper;
+ }
+
+ @Override
+ public long bytesLoaded() {
+ return bytesLoaded;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public void load() throws IOException, InterruptedException {
+ DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ try {
+ // Create and open the input.
+ ExtractorInput input = new DefaultExtractorInput(dataSource,
+ loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+ if (bytesLoaded == 0) {
+ extractorWrapper.init(null);
+ }
+ // Load and decode the initialization data.
+ try {
+ Extractor extractor = extractorWrapper.extractor;
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, null);
+ }
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
+ } finally {
+ bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * An abstract base class for {@link Chunk}s that contain media samples.
+ */
+public abstract class MediaChunk extends Chunk {
+
+ /**
+ * The chunk index.
+ */
+ public final int chunkIndex;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param chunkIndex The index of the chunk.
+ */
+ public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+ int chunkIndex) {
+ super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason,
+ trackSelectionData, startTimeUs, endTimeUs);
+ Assertions.checkNotNull(trackFormat);
+ this.chunkIndex = chunkIndex;
+ }
+
+ /**
+ * Returns the next chunk index.
+ */
+ public int getNextChunkIndex() {
+ return chunkIndex + 1;
+ }
+
+ /**
+ * Returns whether the chunk has been fully loaded.
+ */
+ public abstract boolean isLoadCompleted();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -0,0 +1,114 @@
+/*
+ * 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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link BaseMediaChunk} for chunks consisting of a single raw sample.
+ */
+public final class SingleSampleMediaChunk extends BaseMediaChunk {
+
+ private final int trackType;
+ private final Format sampleFormat;
+
+ private volatile int bytesLoaded;
+ private volatile boolean loadCanceled;
+ private volatile boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param trackFormat See {@link #trackFormat}.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param chunkIndex The index of the chunk.
+ * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*}
+ * constants.
+ * @param sampleFormat The {@link Format} of the sample in the chunk.
+ */
+ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+ int chunkIndex, int trackType, Format sampleFormat) {
+ super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+ endTimeUs, chunkIndex);
+ this.trackType = trackType;
+ this.sampleFormat = sampleFormat;
+ }
+
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ @Override
+ public long bytesLoaded() {
+ return bytesLoaded;
+ }
+
+ // Loadable implementation.
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @SuppressWarnings("NonAtomicVolatileUpdate")
+ @Override
+ public void load() throws IOException, InterruptedException {
+ DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ try {
+ // Create and open the input.
+ long length = dataSource.open(loadDataSpec);
+ if (length != C.LENGTH_UNSET) {
+ length += bytesLoaded;
+ }
+ ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length);
+ BaseMediaChunkOutput output = getOutput();
+ output.setSampleOffsetUs(0);
+ TrackOutput trackOutput = output.track(0, trackType);
+ trackOutput.format(sampleFormat);
+ // Load the sample data.
+ int result = 0;
+ while (result != C.RESULT_END_OF_INPUT) {
+ bytesLoaded += result;
+ result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true);
+ }
+ int sampleSize = bytesLoaded;
+ trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ loadCompleted = true;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.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 com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with
+ * a 128-bit key and PKCS7 padding.
+ * <p>
+ * Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is
+ * designed specifically for reading whole files as defined in an HLS media playlist. For this
+ * reason the implementation is private to the HLS package.
+ */
+/* package */ final class Aes128DataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] encryptionKey;
+ private final byte[] encryptionIv;
+
+ private CipherInputStream cipherInputStream;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param encryptionKey The encryption key.
+ * @param encryptionIv The encryption initialization vector.
+ */
+ public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {
+ this.upstream = upstream;
+ this.encryptionKey = encryptionKey;
+ this.encryptionIv = encryptionIv;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new RuntimeException(e);
+ }
+
+ Key cipherKey = new SecretKeySpec(encryptionKey, "AES");
+ AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);
+
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+
+ cipherInputStream = new CipherInputStream(
+ new DataSourceInputStream(upstream, dataSpec), cipher);
+
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipherInputStream = null;
+ upstream.close();
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ Assertions.checkState(cipherInputStream != null);
+ int bytesRead = cipherInputStream.read(buffer, offset, readLength);
+ if (bytesRead < 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/DefaultHlsDataSourceFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * Default implementation of {@link HlsDataSourceFactory}.
+ */
+public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory {
+
+ private final DataSource.Factory dataSourceFactory;
+
+ /**
+ * @param dataSourceFactory The {@link DataSource.Factory} to use for all data types.
+ */
+ public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ }
+
+ @Override
+ public DataSource createDataSource(int dataType) {
+ return dataSourceFactory.createDataSource();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -0,0 +1,450 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.BehindLiveWindowException;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.Chunk;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.source.chunk.DataChunk;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.UriUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Source of Hls (possibly adaptive) chunks.
+ */
+/* package */ class HlsChunkSource {
+
+ /**
+ * Chunk holder that allows the scheduling of retries.
+ */
+ public static final class HlsChunkHolder {
+
+ public HlsChunkHolder() {
+ clear();
+ }
+
+ /**
+ * The chunk to be loaded next.
+ */
+ public Chunk chunk;
+
+ /**
+ * Indicates that the end of the stream has been reached.
+ */
+ public boolean endOfStream;
+
+ /**
+ * Indicates that the chunk source is waiting for the referred playlist to be refreshed.
+ */
+ public HlsUrl playlist;
+
+ /**
+ * Clears the holder.
+ */
+ public void clear() {
+ chunk = null;
+ endOfStream = false;
+ playlist = null;
+ }
+
+ }
+
+ private final DataSource mediaDataSource;
+ private final DataSource encryptionDataSource;
+ private final TimestampAdjusterProvider timestampAdjusterProvider;
+ private final HlsUrl[] variants;
+ private final HlsPlaylistTracker playlistTracker;
+ private final TrackGroup trackGroup;
+ private final List<Format> muxedCaptionFormats;
+
+ private boolean isTimestampMaster;
+ private byte[] scratchSpace;
+ private IOException fatalError;
+
+ private Uri encryptionKeyUri;
+ private byte[] encryptionKey;
+ private String encryptionIvString;
+ private byte[] encryptionIv;
+
+ // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to
+ // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
+ // in TrackSelection to avoid unexpected behavior.
+ private TrackSelection trackSelection;
+
+ /**
+ * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists.
+ * @param variants The available variants.
+ * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the
+ * chunks.
+ * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If
+ * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the
+ * same provider.
+ * @param muxedCaptionFormats List of muxed caption {@link Format}s.
+ */
+ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants,
+ HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider,
+ List<Format> muxedCaptionFormats) {
+ this.playlistTracker = playlistTracker;
+ this.variants = variants;
+ this.timestampAdjusterProvider = timestampAdjusterProvider;
+ this.muxedCaptionFormats = muxedCaptionFormats;
+ Format[] variantFormats = new Format[variants.length];
+ int[] initialTrackSelection = new int[variants.length];
+ for (int i = 0; i < variants.length; i++) {
+ variantFormats[i] = variants[i].format;
+ initialTrackSelection[i] = i;
+ }
+ mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA);
+ encryptionDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_DRM);
+ trackGroup = new TrackGroup(variantFormats);
+ trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection);
+ }
+
+ /**
+ * If the source is currently having difficulty providing chunks, then this method throws the
+ * underlying error. Otherwise does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ public void maybeThrowError() throws IOException {
+ if (fatalError != null) {
+ throw fatalError;
+ }
+ }
+
+ /**
+ * Returns the track group exposed by the source.
+ */
+ public TrackGroup getTrackGroup() {
+ return trackGroup;
+ }
+
+ /**
+ * Selects tracks for use.
+ *
+ * @param trackSelection The track selection.
+ */
+ public void selectTracks(TrackSelection trackSelection) {
+ this.trackSelection = trackSelection;
+ }
+
+ /**
+ * Resets the source.
+ */
+ public void reset() {
+ fatalError = null;
+ }
+
+ /**
+ * Sets whether this chunk source is responsible for initializing timestamp adjusters.
+ *
+ * @param isTimestampMaster True if this chunk source is responsible for initializing timestamp
+ * adjusters.
+ */
+ public void setIsTimestampMaster(boolean isTimestampMaster) {
+ this.isTimestampMaster = isTimestampMaster;
+ }
+
+ /**
+ * Returns the next chunk to load.
+ * <p>
+ * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has
+ * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but
+ * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
+ * contain the {@link HlsUrl} that refers to the playlist that needs refreshing.
+ *
+ * @param previous The most recently loaded media chunk.
+ * @param playbackPositionUs The current playback position. If {@code previous} is null then this
+ * parameter is the position from which playback is expected to start (or restart) and hence
+ * should be interpreted as a seek position.
+ * @param out A holder to populate.
+ */
+ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) {
+ int oldVariantIndex = previous == null ? C.INDEX_UNSET
+ : trackGroup.indexOf(previous.trackFormat);
+ // Use start time of the previous chunk rather than its end time because switching format will
+ // require downloading overlapping segments.
+ long bufferedDurationUs = previous == null ? 0
+ : Math.max(0, previous.startTimeUs - playbackPositionUs);
+
+ // Select the variant.
+ trackSelection.updateSelectedTrack(bufferedDurationUs);
+ int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup();
+
+ boolean switchingVariant = oldVariantIndex != selectedVariantIndex;
+ HlsUrl selectedUrl = variants[selectedVariantIndex];
+ if (!playlistTracker.isSnapshotValid(selectedUrl)) {
+ out.playlist = selectedUrl;
+ // Retry when playlist is refreshed.
+ return;
+ }
+ HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
+
+ // Select the chunk.
+ int chunkMediaSequence;
+ if (previous == null || switchingVariant) {
+ long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs;
+ if (!mediaPlaylist.hasEndTag && targetPositionUs > mediaPlaylist.getEndTimeUs()) {
+ // If the playlist is too old to contain the chunk, we need to refresh it.
+ chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
+ } else {
+ chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
+ targetPositionUs - mediaPlaylist.startTimeUs, true,
+ !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
+ if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
+ // We try getting the next chunk without adapting in case that's the reason for falling
+ // behind the live window.
+ selectedVariantIndex = oldVariantIndex;
+ selectedUrl = variants[selectedVariantIndex];
+ mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
+ chunkMediaSequence = previous.getNextChunkIndex();
+ }
+ }
+ } else {
+ chunkMediaSequence = previous.getNextChunkIndex();
+ }
+ if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
+ fatalError = new BehindLiveWindowException();
+ return;
+ }
+
+ int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
+ if (chunkIndex >= mediaPlaylist.segments.size()) {
+ if (mediaPlaylist.hasEndTag) {
+ out.endOfStream = true;
+ } else /* Live */ {
+ out.playlist = selectedUrl;
+ }
+ return;
+ }
+
+ // Handle encryption.
+ HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
+
+ // Check if encryption is specified.
+ if (segment.isEncrypted) {
+ Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
+ if (!keyUri.equals(encryptionKeyUri)) {
+ // Encryption is specified and the key has changed.
+ out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex,
+ trackSelection.getSelectionReason(), trackSelection.getSelectionData());
+ return;
+ }
+ if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) {
+ setEncryptionData(keyUri, segment.encryptionIV, encryptionKey);
+ }
+ } else {
+ clearEncryptionData();
+ }
+
+ DataSpec initDataSpec = null;
+ Segment initSegment = mediaPlaylist.initializationSegment;
+ if (initSegment != null) {
+ Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
+ initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset,
+ initSegment.byterangeLength, null);
+ }
+
+ // Compute start time of the next chunk.
+ long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
+ int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ + segment.relativeDiscontinuitySequence;
+ TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
+ discontinuitySequence);
+
+ // Configure the data source and spec for the chunk.
+ Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
+ DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
+ null);
+ out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl,
+ muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
+ startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
+ isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv);
+ }
+
+ /**
+ * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
+ * source.
+ *
+ * @param chunk The chunk whose load has been completed.
+ */
+ public void onChunkLoadCompleted(Chunk chunk) {
+ if (chunk instanceof EncryptionKeyChunk) {
+ EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;
+ scratchSpace = encryptionKeyChunk.getDataHolder();
+ setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv,
+ encryptionKeyChunk.getResult());
+ }
+ }
+
+ /**
+ * Called when the {@link HlsSampleStreamWrapper} encounters an error loading a chunk obtained
+ * from this source.
+ *
+ * @param chunk The chunk whose load encountered the error.
+ * @param cancelable Whether the load can be canceled.
+ * @param error The error.
+ * @return Whether the load should be canceled.
+ */
+ public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException error) {
+ return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
+ trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), error);
+ }
+
+ /**
+ * Called when a playlist is blacklisted.
+ *
+ * @param url The url that references the blacklisted playlist.
+ * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted.
+ */
+ public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
+ int trackGroupIndex = trackGroup.indexOf(url.format);
+ if (trackGroupIndex != C.INDEX_UNSET) {
+ int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex);
+ if (trackSelectionIndex != C.INDEX_UNSET) {
+ trackSelection.blacklist(trackSelectionIndex, blacklistMs);
+ }
+ }
+ }
+
+ // Private methods.
+
+ private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex,
+ int trackSelectionReason, Object trackSelectionData) {
+ DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP);
+ return new EncryptionKeyChunk(encryptionDataSource, dataSpec, variants[variantIndex].format,
+ trackSelectionReason, trackSelectionData, scratchSpace, iv);
+ }
+
+ private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) {
+ String trimmedIv;
+ if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
+ trimmedIv = iv.substring(2);
+ } else {
+ trimmedIv = iv;
+ }
+
+ byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray();
+ byte[] ivDataWithPadding = new byte[16];
+ int offset = ivData.length > 16 ? ivData.length - 16 : 0;
+ System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+ + offset, ivData.length - offset);
+
+ encryptionKeyUri = keyUri;
+ encryptionKey = secretKey;
+ encryptionIvString = iv;
+ encryptionIv = ivDataWithPadding;
+ }
+
+ private void clearEncryptionData() {
+ encryptionKeyUri = null;
+ encryptionKey = null;
+ encryptionIvString = null;
+ encryptionIv = null;
+ }
+
+ // Private classes.
+
+ /**
+ * A {@link TrackSelection} to use for initialization.
+ */
+ private static final class InitializationTrackSelection extends BaseTrackSelection {
+
+ private int selectedIndex;
+
+ public InitializationTrackSelection(TrackGroup group, int[] tracks) {
+ super(group, tracks);
+ selectedIndex = indexOf(group.getFormat(0));
+ }
+
+ @Override
+ public void updateSelectedTrack(long bufferedDurationUs) {
+ long nowMs = SystemClock.elapsedRealtime();
+ if (!isBlacklisted(selectedIndex, nowMs)) {
+ return;
+ }
+ // Try from lowest bitrate to highest.
+ for (int i = length - 1; i >= 0; i--) {
+ if (!isBlacklisted(i, nowMs)) {
+ selectedIndex = i;
+ return;
+ }
+ }
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_UNKNOWN;
+ }
+
+ @Override
+ public Object getSelectionData() {
+ return null;
+ }
+
+ }
+
+ private static final class EncryptionKeyChunk extends DataChunk {
+
+ public final String iv;
+
+ private byte[] result;
+
+ public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+ int trackSelectionReason, Object trackSelectionData, byte[] scratchSpace, String iv) {
+ super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason,
+ trackSelectionData, scratchSpace);
+ this.iv = iv;
+ }
+
+ @Override
+ protected void consume(byte[] data, int limit) throws IOException {
+ result = Arrays.copyOf(data, limit);
+ }
+
+ public byte[] getResult() {
+ return result;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsDataSourceFactory.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+
+/**
+ * Creates {@link DataSource}s for HLS playlists, encryption and media chunks.
+ */
+public interface HlsDataSourceFactory {
+
+ /**
+ * Creates a {@link DataSource} for the given data type.
+ *
+ * @param dataType The data type for which the {@link DataSource} will be used. One of {@link C}
+ * {@code .DATA_TYPE_*} constants.
+ * @return A {@link DataSource} for the given data type.
+ */
+ DataSource createDataSource(int dataType);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsManifest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+
+/**
+ * Holds a master playlist along with a snapshot of one of its media playlists.
+ */
+public final class HlsManifest {
+
+ /**
+ * The master playlist of an HLS stream.
+ */
+ public final HlsMasterPlaylist masterPlaylist;
+ /**
+ * A snapshot of a media playlist referred to by {@link #masterPlaylist}.
+ */
+ public final HlsMediaPlaylist mediaPlaylist;
+
+ /**
+ * @param masterPlaylist The master playlist.
+ * @param mediaPlaylist The media playlist.
+ */
+ HlsManifest(HlsMasterPlaylist masterPlaylist, HlsMediaPlaylist mediaPlaylist) {
+ this.masterPlaylist = masterPlaylist;
+ this.mediaPlaylist = mediaPlaylist;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -0,0 +1,400 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.metadata.id3.PrivFrame;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An HLS {@link MediaChunk}.
+ */
+/* package */ final class HlsMediaChunk extends MediaChunk {
+
+ private static final AtomicInteger UID_SOURCE = new AtomicInteger();
+
+ private static final String PRIV_TIMESTAMP_FRAME_OWNER =
+ "com.apple.streaming.transportStreamTimestamp";
+
+ private static final String AAC_FILE_EXTENSION = ".aac";
+ private static final String AC3_FILE_EXTENSION = ".ac3";
+ private static final String EC3_FILE_EXTENSION = ".ec3";
+ private static final String MP3_FILE_EXTENSION = ".mp3";
+ private static final String MP4_FILE_EXTENSION = ".mp4";
+ private static final String M4_FILE_EXTENSION_PREFIX = ".m4";
+ private static final String VTT_FILE_EXTENSION = ".vtt";
+ private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
+
+ /**
+ * A unique identifier for the chunk.
+ */
+ public final int uid;
+
+ /**
+ * The discontinuity sequence number of the chunk.
+ */
+ public final int discontinuitySequenceNumber;
+
+ /**
+ * The url of the playlist from which this chunk was obtained.
+ */
+ public final HlsUrl hlsUrl;
+
+ private final DataSource initDataSource;
+ private final DataSpec initDataSpec;
+ private final boolean isEncrypted;
+ private final boolean isMasterTimestampSource;
+ private final TimestampAdjuster timestampAdjuster;
+ private final String lastPathSegment;
+ private final Extractor previousExtractor;
+ private final boolean shouldSpliceIn;
+ private final boolean needNewExtractor;
+ private final List<Format> muxedCaptionFormats;
+
+ private final boolean isPackedAudio;
+ private final Id3Decoder id3Decoder;
+ private final ParsableByteArray id3Data;
+
+ private Extractor extractor;
+ private int initSegmentBytesLoaded;
+ private int bytesLoaded;
+ private boolean initLoadCompleted;
+ private HlsSampleStreamWrapper extractorOutput;
+ private volatile boolean loadCanceled;
+ private volatile boolean loadCompleted;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded.
+ * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null.
+ * @param hlsUrl The url of the playlist from which this chunk was obtained.
+ * @param muxedCaptionFormats List of muxed caption {@link Format}s.
+ * @param trackSelectionReason See {@link #trackSelectionReason}.
+ * @param trackSelectionData See {@link #trackSelectionData}.
+ * @param startTimeUs The start time of the chunk in microseconds.
+ * @param endTimeUs The end time of the chunk in microseconds.
+ * @param chunkIndex The media sequence number of the chunk.
+ * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk.
+ * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
+ * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
+ * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
+ * @param encryptionKey For AES encryption chunks, the encryption key.
+ * @param encryptionIv For AES encryption chunks, the encryption initialization vector.
+ */
+ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
+ HlsUrl hlsUrl, List<Format> muxedCaptionFormats, int trackSelectionReason,
+ Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex,
+ int discontinuitySequenceNumber, boolean isMasterTimestampSource,
+ TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey,
+ byte[] encryptionIv) {
+ super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
+ trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
+ this.discontinuitySequenceNumber = discontinuitySequenceNumber;
+ this.initDataSpec = initDataSpec;
+ this.hlsUrl = hlsUrl;
+ this.muxedCaptionFormats = muxedCaptionFormats;
+ this.isMasterTimestampSource = isMasterTimestampSource;
+ this.timestampAdjuster = timestampAdjuster;
+ // Note: this.dataSource and dataSource may be different.
+ this.isEncrypted = this.dataSource instanceof Aes128DataSource;
+ lastPathSegment = dataSpec.uri.getLastPathSegment();
+ isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION)
+ || lastPathSegment.endsWith(AC3_FILE_EXTENSION)
+ || lastPathSegment.endsWith(EC3_FILE_EXTENSION)
+ || lastPathSegment.endsWith(MP3_FILE_EXTENSION);
+ if (previousChunk != null) {
+ id3Decoder = previousChunk.id3Decoder;
+ id3Data = previousChunk.id3Data;
+ previousExtractor = previousChunk.extractor;
+ shouldSpliceIn = previousChunk.hlsUrl != hlsUrl;
+ needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
+ || shouldSpliceIn;
+ } else {
+ id3Decoder = isPackedAudio ? new Id3Decoder() : null;
+ id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null;
+ previousExtractor = null;
+ shouldSpliceIn = false;
+ needNewExtractor = true;
+ }
+ initDataSource = dataSource;
+ uid = UID_SOURCE.getAndIncrement();
+ }
+
+ /**
+ * Initializes the chunk for loading, setting the {@link HlsSampleStreamWrapper} that will receive
+ * samples as they are loaded.
+ *
+ * @param output The output that will receive the loaded samples.
+ */
+ public void init(HlsSampleStreamWrapper output) {
+ extractorOutput = output;
+ output.init(uid, shouldSpliceIn);
+ }
+
+ @Override
+ public boolean isLoadCompleted() {
+ return loadCompleted;
+ }
+
+ @Override
+ public long bytesLoaded() {
+ return bytesLoaded;
+ }
+
+ // Loadable implementation
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ if (extractor == null && !isPackedAudio) {
+ // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction.
+ extractor = createExtractor();
+ }
+ maybeLoadInitData();
+ if (!loadCanceled) {
+ loadMedia();
+ }
+ }
+
+ // Internal loading methods.
+
+ private void maybeLoadInitData() throws IOException, InterruptedException {
+ if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) {
+ // According to spec, for packed audio, initDataSpec is expected to be null.
+ return;
+ }
+ DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
+ try {
+ ExtractorInput input = new DefaultExtractorInput(initDataSource,
+ initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec));
+ try {
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, null);
+ }
+ } finally {
+ initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ initLoadCompleted = true;
+ }
+
+ private void loadMedia() throws IOException, InterruptedException {
+ // If we previously fed part of this chunk to the extractor, we need to skip it this time. For
+ // encrypted content we need to skip the data by reading it through the source, so as to ensure
+ // correct decryption of the remainder of the chunk. For clear content, we can request the
+ // remainder of the chunk directly.
+ DataSpec loadDataSpec;
+ boolean skipLoadedBytes;
+ if (isEncrypted) {
+ loadDataSpec = dataSpec;
+ skipLoadedBytes = bytesLoaded != 0;
+ } else {
+ loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+ skipLoadedBytes = false;
+ }
+ if (!isMasterTimestampSource) {
+ timestampAdjuster.waitUntilInitialized();
+ } else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) {
+ // We're the master and we haven't set the desired first sample timestamp yet.
+ timestampAdjuster.setFirstSampleTimestampUs(startTimeUs);
+ }
+ try {
+ ExtractorInput input = new DefaultExtractorInput(dataSource,
+ loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+ if (extractor == null) {
+ // Media segment format is packed audio.
+ long id3Timestamp = peekId3PrivTimestamp(input);
+ extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET
+ ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs);
+ }
+ if (skipLoadedBytes) {
+ input.skipFully(bytesLoaded);
+ }
+ try {
+ int result = Extractor.RESULT_CONTINUE;
+ while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+ result = extractor.read(input, null);
+ }
+ } finally {
+ bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ loadCompleted = true;
+ }
+
+ /**
+ * Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined
+ * in the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not
+ * found. This method only modifies the peek position.
+ *
+ * @param input The {@link ExtractorInput} to obtain the PRIV frame from.
+ * @return The parsed, adjusted timestamp in microseconds
+ * @throws IOException If an error occurred peeking from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
+ input.resetPeekPosition();
+ if (!input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) {
+ return C.TIME_UNSET;
+ }
+ id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
+ int id = id3Data.readUnsignedInt24();
+ if (id != Id3Decoder.ID3_TAG) {
+ return C.TIME_UNSET;
+ }
+ id3Data.skipBytes(3); // version(2), flags(1).
+ int id3Size = id3Data.readSynchSafeInt();
+ int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH;
+ if (requiredCapacity > id3Data.capacity()) {
+ byte[] data = id3Data.data;
+ id3Data.reset(requiredCapacity);
+ System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+ }
+ if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) {
+ return C.TIME_UNSET;
+ }
+ Metadata metadata = id3Decoder.decode(id3Data.data, id3Size);
+ if (metadata == null) {
+ return C.TIME_UNSET;
+ }
+ int metadataLength = metadata.length();
+ for (int i = 0; i < metadataLength; i++) {
+ Metadata.Entry frame = metadata.get(i);
+ if (frame instanceof PrivFrame) {
+ PrivFrame privFrame = (PrivFrame) frame;
+ if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
+ System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */);
+ id3Data.reset(8);
+ return id3Data.readLong();
+ }
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+ // Internal factory methods.
+
+ /**
+ * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in
+ * order to decrypt the loaded data. Else returns the original.
+ */
+ private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey,
+ byte[] encryptionIv) {
+ if (encryptionKey == null || encryptionIv == null) {
+ return dataSource;
+ }
+ return new Aes128DataSource(dataSource, encryptionKey, encryptionIv);
+ }
+
+ private Extractor createExtractor() {
+ // Select the extractor that will read the chunk.
+ Extractor extractor;
+ boolean usingNewExtractor = true;
+ if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType)
+ || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
+ || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
+ extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster);
+ } else if (!needNewExtractor) {
+ // Only reuse TS and fMP4 extractors.
+ usingNewExtractor = false;
+ extractor = previousExtractor;
+ } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)
+ || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) {
+ extractor = new FragmentedMp4Extractor(0, timestampAdjuster);
+ } else {
+ // MPEG-2 TS segments, but we need a new extractor.
+ // This flag ensures the change of pid between streams does not affect the sample queues.
+ @DefaultTsPayloadReaderFactory.Flags
+ int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM;
+ if (!muxedCaptionFormats.isEmpty()) {
+ // The playlist declares closed caption renditions, we should ignore descriptors.
+ esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
+ }
+ String codecs = trackFormat.codecs;
+ if (!TextUtils.isEmpty(codecs)) {
+ // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
+ // exist. If we know from the codec attribute that they don't exist, then we can
+ // explicitly ignore them even if they're declared.
+ if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
+ esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
+ }
+ if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
+ esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
+ }
+ }
+ extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster,
+ new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats));
+ }
+ if (usingNewExtractor) {
+ extractor.init(extractorOutput);
+ }
+ return extractor;
+ }
+
+ private Extractor buildPackedAudioExtractor(long startTimeUs) {
+ Extractor extractor;
+ if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
+ extractor = new AdtsExtractor(startTimeUs);
+ } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
+ || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
+ extractor = new Ac3Extractor(startTimeUs);
+ } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
+ extractor = new Mp3Extractor(0, startTimeUs);
+ } else {
+ throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment);
+ }
+ extractor.init(extractorOutput);
+ return extractor;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
@@ -0,0 +1,383 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.os.Handler;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.CompositeSequenceableLoader;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/**
+ * A {@link MediaPeriod} that loads an HLS stream.
+ */
+public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
+ HlsPlaylistTracker.PlaylistEventListener {
+
+ private final HlsPlaylistTracker playlistTracker;
+ private final HlsDataSourceFactory dataSourceFactory;
+ private final int minLoadableRetryCount;
+ private final EventDispatcher eventDispatcher;
+ private final Allocator allocator;
+ private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
+ private final TimestampAdjusterProvider timestampAdjusterProvider;
+ private final Handler continueLoadingHandler;
+ private final long preparePositionUs;
+
+ private Callback callback;
+ private int pendingPrepareCount;
+ private boolean seenFirstTrackSelection;
+ private TrackGroupArray trackGroups;
+ private HlsSampleStreamWrapper[] sampleStreamWrappers;
+ private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
+ private CompositeSequenceableLoader sequenceableLoader;
+
+ public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory,
+ int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator,
+ long positionUs) {
+ this.playlistTracker = playlistTracker;
+ this.dataSourceFactory = dataSourceFactory;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ this.eventDispatcher = eventDispatcher;
+ this.allocator = allocator;
+ streamWrapperIndices = new IdentityHashMap<>();
+ timestampAdjusterProvider = new TimestampAdjusterProvider();
+ continueLoadingHandler = new Handler();
+ preparePositionUs = positionUs;
+ }
+
+ public void release() {
+ playlistTracker.removeListener(this);
+ continueLoadingHandler.removeCallbacksAndMessages(null);
+ if (sampleStreamWrappers != null) {
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ sampleStreamWrapper.release();
+ }
+ }
+ }
+
+ @Override
+ public void prepare(Callback callback) {
+ playlistTracker.addListener(this);
+ this.callback = callback;
+ buildAndPrepareSampleStreamWrappers();
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ if (sampleStreamWrappers != null) {
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ sampleStreamWrapper.maybeThrowPrepareError();
+ }
+ }
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return trackGroups;
+ }
+
+ @Override
+ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+ // Map each selection and stream onto a child period index.
+ int[] streamChildIndices = new int[selections.length];
+ int[] selectionChildIndices = new int[selections.length];
+ for (int i = 0; i < selections.length; i++) {
+ streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
+ : streamWrapperIndices.get(streams[i]);
+ selectionChildIndices[i] = C.INDEX_UNSET;
+ if (selections[i] != null) {
+ TrackGroup trackGroup = selections[i].getTrackGroup();
+ for (int j = 0; j < sampleStreamWrappers.length; j++) {
+ if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != C.INDEX_UNSET) {
+ selectionChildIndices[i] = j;
+ break;
+ }
+ }
+ }
+ }
+ boolean selectedNewTracks = false;
+ streamWrapperIndices.clear();
+ // Select tracks for each child, copying the resulting streams back into a new streams array.
+ SampleStream[] newStreams = new SampleStream[selections.length];
+ SampleStream[] childStreams = new SampleStream[selections.length];
+ TrackSelection[] childSelections = new TrackSelection[selections.length];
+ ArrayList<HlsSampleStreamWrapper> enabledSampleStreamWrapperList = new ArrayList<>(
+ sampleStreamWrappers.length);
+ for (int i = 0; i < sampleStreamWrappers.length; i++) {
+ for (int j = 0; j < selections.length; j++) {
+ childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
+ childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
+ }
+ selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections,
+ mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection);
+ boolean wrapperEnabled = false;
+ for (int j = 0; j < selections.length; j++) {
+ if (selectionChildIndices[j] == i) {
+ // Assert that the child provided a stream for the selection.
+ Assertions.checkState(childStreams[j] != null);
+ newStreams[j] = childStreams[j];
+ wrapperEnabled = true;
+ streamWrapperIndices.put(childStreams[j], i);
+ } else if (streamChildIndices[j] == i) {
+ // Assert that the child cleared any previous stream.
+ Assertions.checkState(childStreams[j] == null);
+ }
+ }
+ if (wrapperEnabled) {
+ enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]);
+ }
+ }
+ // Copy the new streams back into the streams array.
+ System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
+ // Update the local state.
+ enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()];
+ enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers);
+
+ // The first enabled sample stream wrapper is responsible for intializing the timestamp
+ // adjuster. This way, if present, variants are responsible. Otherwise, audio renditions are.
+ // If only subtitles are present, then text renditions are used for timestamp adjustment
+ // initialization.
+ if (enabledSampleStreamWrappers.length > 0) {
+ enabledSampleStreamWrappers[0].setIsTimestampMaster(true);
+ for (int i = 1; i < enabledSampleStreamWrappers.length; i++) {
+ enabledSampleStreamWrappers[i].setIsTimestampMaster(false);
+ }
+ }
+
+ sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers);
+ if (seenFirstTrackSelection && selectedNewTracks) {
+ seekToUs(positionUs);
+ // We'll need to reset renderers consuming from all streams due to the seek.
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null) {
+ streamResetFlags[i] = true;
+ }
+ }
+ }
+ seenFirstTrackSelection = true;
+ return positionUs;
+ }
+
+ @Override
+ public void discardBuffer(long positionUs) {
+ // Do nothing.
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return sequenceableLoader.continueLoading(positionUs);
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return sequenceableLoader.getNextLoadPositionUs();
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ return C.TIME_UNSET;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ long bufferedPositionUs = Long.MAX_VALUE;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
+ long rendererBufferedPositionUs = sampleStreamWrapper.getBufferedPositionUs();
+ if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+ bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+ }
+ }
+ return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ timestampAdjusterProvider.reset();
+ for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
+ sampleStreamWrapper.seekTo(positionUs);
+ }
+ return positionUs;
+ }
+
+ // HlsSampleStreamWrapper.Callback implementation.
+
+ @Override
+ public void onPrepared() {
+ if (--pendingPrepareCount > 0) {
+ return;
+ }
+
+ int totalTrackGroupCount = 0;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length;
+ }
+ TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
+ int trackGroupIndex = 0;
+ for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+ int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length;
+ for (int j = 0; j < wrapperTrackGroupCount; j++) {
+ trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j);
+ }
+ }
+ trackGroups = new TrackGroupArray(trackGroupArray);
+ callback.onPrepared(this);
+ }
+
+ @Override
+ public void onPlaylistRefreshRequired(HlsUrl url) {
+ playlistTracker.refreshPlaylist(url);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) {
+ if (trackGroups == null) {
+ // Still preparing.
+ return;
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+
+ // PlaylistListener implementation.
+
+ @Override
+ public void onPlaylistChanged() {
+ continuePreparingOrLoading();
+ }
+
+ @Override
+ public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
+ for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {
+ streamWrapper.onPlaylistBlacklisted(url, blacklistMs);
+ }
+ continuePreparingOrLoading();
+ }
+
+ // Internal methods.
+
+ private void buildAndPrepareSampleStreamWrappers() {
+ HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist();
+ // Build the default stream wrapper.
+ List<HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants);
+ ArrayList<HlsUrl> definiteVideoVariants = new ArrayList<>();
+ ArrayList<HlsUrl> definiteAudioOnlyVariants = new ArrayList<>();
+ for (int i = 0; i < selectedVariants.size(); i++) {
+ HlsUrl variant = selectedVariants.get(i);
+ if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) {
+ definiteVideoVariants.add(variant);
+ } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) {
+ definiteAudioOnlyVariants.add(variant);
+ }
+ }
+ if (!definiteVideoVariants.isEmpty()) {
+ // We've identified some variants as definitely containing video. Assume variants within the
+ // master playlist are marked consistently, and hence that we have the full set. Filter out
+ // any other variants, which are likely to be audio only.
+ selectedVariants = definiteVideoVariants;
+ } else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) {
+ // We've identified some variants, but not all, as being audio only. Filter them out to leave
+ // the remaining variants, which are likely to contain video.
+ selectedVariants.removeAll(definiteAudioOnlyVariants);
+ } else {
+ // Leave the enabled variants unchanged. They're likely either all video or all audio.
+ }
+ List<HlsUrl> audioRenditions = masterPlaylist.audios;
+ List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
+ sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size()
+ + subtitleRenditions.size()];
+ int currentWrapperIndex = 0;
+ pendingPrepareCount = sampleStreamWrappers.length;
+
+ Assertions.checkArgument(!selectedVariants.isEmpty());
+ HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()];
+ selectedVariants.toArray(variants);
+ HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
+ variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats);
+ sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
+ sampleStreamWrapper.setIsTimestampMaster(true);
+ sampleStreamWrapper.continuePreparing();
+
+ // TODO: Build video stream wrappers here.
+
+ // Build audio stream wrappers.
+ for (int i = 0; i < audioRenditions.size(); i++) {
+ sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
+ new HlsUrl[] {audioRenditions.get(i)}, null, Collections.<Format>emptyList());
+ sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
+ sampleStreamWrapper.continuePreparing();
+ }
+
+ // Build subtitle stream wrappers.
+ for (int i = 0; i < subtitleRenditions.size(); i++) {
+ HlsUrl url = subtitleRenditions.get(i);
+ sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null,
+ Collections.<Format>emptyList());
+ sampleStreamWrapper.prepareSingleTrack(url.format);
+ sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
+ }
+ }
+
+ private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,
+ Format muxedAudioFormat, List<Format> muxedCaptionFormats) {
+ HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants,
+ dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats);
+ return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator,
+ preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher);
+ }
+
+ private void continuePreparingOrLoading() {
+ if (trackGroups != null) {
+ callback.onContinueLoadingRequested(this);
+ } else {
+ // Some of the wrappers were waiting for their media playlist to prepare.
+ for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) {
+ wrapper.continuePreparing();
+ }
+ }
+ }
+
+ private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) {
+ String codecs = variant.format.codecs;
+ if (TextUtils.isEmpty(codecs)) {
+ return false;
+ }
+ String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
+ for (String codec : codecArray) {
+ if (codec.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -0,0 +1,136 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * An HLS {@link MediaSource}.
+ */
+public final class HlsMediaSource implements MediaSource,
+ HlsPlaylistTracker.PrimaryPlaylistListener {
+
+ /**
+ * The default minimum number of times to retry loading data prior to failing.
+ */
+ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+
+ private final Uri manifestUri;
+ private final HlsDataSourceFactory dataSourceFactory;
+ private final int minLoadableRetryCount;
+ private final EventDispatcher eventDispatcher;
+
+ private HlsPlaylistTracker playlistTracker;
+ private Listener sourceListener;
+
+ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler,
+ AdaptiveMediaSourceEventListener eventListener) {
+ this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler,
+ eventListener);
+ }
+
+ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
+ int minLoadableRetryCount, Handler eventHandler,
+ AdaptiveMediaSourceEventListener eventListener) {
+ this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount,
+ eventHandler, eventListener);
+ }
+
+ public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory,
+ int minLoadableRetryCount, Handler eventHandler,
+ AdaptiveMediaSourceEventListener eventListener) {
+ this.manifestUri = manifestUri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ }
+
+ @Override
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ Assertions.checkState(playlistTracker == null);
+ playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
+ minLoadableRetryCount, this);
+ sourceListener = listener;
+ playlistTracker.start();
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ playlistTracker.maybeThrowPlaylistRefreshError();
+ }
+
+ @Override
+ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+ Assertions.checkArgument(index == 0);
+ return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount,
+ eventDispatcher, allocator, positionUs);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ ((HlsMediaPeriod) mediaPeriod).release();
+ }
+
+ @Override
+ public void releaseSource() {
+ if (playlistTracker != null) {
+ playlistTracker.release();
+ playlistTracker = null;
+ }
+ sourceListener = null;
+ }
+
+ @Override
+ public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
+ SinglePeriodTimeline timeline;
+ long windowDefaultStartPositionUs = playlist.startOffsetUs;
+ if (playlistTracker.isLive()) {
+ long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs)
+ : C.TIME_UNSET;
+ List<HlsMediaPlaylist.Segment> segments = playlist.segments;
+ if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+ windowDefaultStartPositionUs = segments.isEmpty() ? 0
+ : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;
+ }
+ timeline = new SinglePeriodTimeline(periodDurationUs, playlist.durationUs,
+ playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag);
+ } else /* not live */ {
+ if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+ windowDefaultStartPositionUs = 0;
+ }
+ timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs,
+ playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false);
+ }
+ sourceListener.onSourceInfoRefreshed(timeline,
+ new HlsManifest(playlistTracker.getMasterPlaylist(), playlist));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import java.io.IOException;
+
+/**
+ * {@link SampleStream} for a particular track group in HLS.
+ */
+/* package */ final class HlsSampleStream implements SampleStream {
+
+ public final int group;
+
+ private final HlsSampleStreamWrapper sampleStreamWrapper;
+
+ public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int group) {
+ this.sampleStreamWrapper = sampleStreamWrapper;
+ this.group = group;
+ }
+
+ @Override
+ public boolean isReady() {
+ return sampleStreamWrapper.isReady(group);
+ }
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ sampleStreamWrapper.maybeThrowError();
+ }
+
+ @Override
+ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
+ return sampleStreamWrapper.readData(group, formatHolder, buffer, requireFormat);
+ }
+
+ @Override
+ public void skipData(long positionUs) {
+ sampleStreamWrapper.skipData(group, positionUs);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -0,0 +1,671 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.SequenceableLoader;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.chunk.Chunk;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+import java.util.LinkedList;
+
+/**
+ * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
+ * {@link SampleStream}s from which the loaded media can be consumed.
+ */
+/* package */ final class HlsSampleStreamWrapper implements Loader.Callback<Chunk>,
+ SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {
+
+ /**
+ * A callback to be notified of events.
+ */
+ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWrapper> {
+
+ /**
+ * Called when the wrapper has been prepared.
+ */
+ void onPrepared();
+
+ /**
+ * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the
+ * given url changes.
+ */
+ void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl playlistUrl);
+
+ }
+
+ private static final int PRIMARY_TYPE_NONE = 0;
+ private static final int PRIMARY_TYPE_TEXT = 1;
+ private static final int PRIMARY_TYPE_AUDIO = 2;
+ private static final int PRIMARY_TYPE_VIDEO = 3;
+
+ private final int trackType;
+ private final Callback callback;
+ private final HlsChunkSource chunkSource;
+ private final Allocator allocator;
+ private final Format muxedAudioFormat;
+ private final int minLoadableRetryCount;
+ private final Loader loader;
+ private final EventDispatcher eventDispatcher;
+ private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
+ private final SparseArray<DefaultTrackOutput> sampleQueues;
+ private final LinkedList<HlsMediaChunk> mediaChunks;
+ private final Runnable maybeFinishPrepareRunnable;
+ private final Handler handler;
+
+ private boolean sampleQueuesBuilt;
+ private boolean prepared;
+ private int enabledTrackCount;
+ private Format downstreamTrackFormat;
+ private int upstreamChunkUid;
+ private boolean released;
+
+ // Tracks are complicated in HLS. See documentation of buildTracks for details.
+ // Indexed by track (as exposed by this source).
+ private TrackGroupArray trackGroups;
+ private int primaryTrackGroupIndex;
+ // Indexed by group.
+ private boolean[] groupEnabledStates;
+
+ private long lastSeekPositionUs;
+ private long pendingResetPositionUs;
+
+ private boolean loadingFinished;
+
+ /**
+ * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @param callback A callback for the wrapper.
+ * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained.
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+ * @param positionUs The position from which to start loading media.
+ * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist.
+ * @param minLoadableRetryCount The minimum number of times that the source should retry a load
+ * before propagating an error.
+ * @param eventDispatcher A dispatcher to notify of events.
+ */
+ public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource,
+ Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount,
+ EventDispatcher eventDispatcher) {
+ this.trackType = trackType;
+ this.callback = callback;
+ this.chunkSource = chunkSource;
+ this.allocator = allocator;
+ this.muxedAudioFormat = muxedAudioFormat;
+ this.minLoadableRetryCount = minLoadableRetryCount;
+ this.eventDispatcher = eventDispatcher;
+ loader = new Loader("Loader:HlsSampleStreamWrapper");
+ nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
+ sampleQueues = new SparseArray<>();
+ mediaChunks = new LinkedList<>();
+ maybeFinishPrepareRunnable = new Runnable() {
+ @Override
+ public void run() {
+ maybeFinishPrepare();
+ }
+ };
+ handler = new Handler();
+ lastSeekPositionUs = positionUs;
+ pendingResetPositionUs = positionUs;
+ }
+
+ public void continuePreparing() {
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ }
+ }
+
+ /**
+ * Prepares a sample stream wrapper for which the master playlist provides enough information to
+ * prepare.
+ */
+ public void prepareSingleTrack(Format format) {
+ track(0, C.TRACK_TYPE_UNKNOWN).format(format);
+ sampleQueuesBuilt = true;
+ maybeFinishPrepare();
+ }
+
+ public void maybeThrowPrepareError() throws IOException {
+ maybeThrowError();
+ }
+
+ public TrackGroupArray getTrackGroups() {
+ return trackGroups;
+ }
+
+ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+ SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) {
+ Assertions.checkState(prepared);
+ // Disable old tracks.
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+ int group = ((HlsSampleStream) streams[i]).group;
+ setTrackGroupEnabledState(group, false);
+ sampleQueues.valueAt(group).disable();
+ streams[i] = null;
+ }
+ }
+ // Enable new tracks.
+ TrackSelection primaryTrackSelection = null;
+ boolean selectedNewTracks = false;
+ for (int i = 0; i < selections.length; i++) {
+ if (streams[i] == null && selections[i] != null) {
+ TrackSelection selection = selections[i];
+ int group = trackGroups.indexOf(selection.getTrackGroup());
+ setTrackGroupEnabledState(group, true);
+ if (group == primaryTrackGroupIndex) {
+ primaryTrackSelection = selection;
+ chunkSource.selectTracks(selection);
+ }
+ streams[i] = new HlsSampleStream(this, group);
+ streamResetFlags[i] = true;
+ selectedNewTracks = true;
+ }
+ }
+ if (isFirstTrackSelection) {
+ // At the time of the first track selection all queues will be enabled, so we need to disable
+ // any that are no longer required.
+ int sampleQueueCount = sampleQueues.size();
+ for (int i = 0; i < sampleQueueCount; i++) {
+ if (!groupEnabledStates[i]) {
+ sampleQueues.valueAt(i).disable();
+ }
+ }
+ if (primaryTrackSelection != null && !mediaChunks.isEmpty()) {
+ primaryTrackSelection.updateSelectedTrack(0);
+ int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat);
+ if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {
+ // The loaded preparation chunk does match the selection. We discard it.
+ seekTo(lastSeekPositionUs);
+ }
+ }
+ }
+ // Cancel requests if necessary.
+ if (enabledTrackCount == 0) {
+ chunkSource.reset();
+ downstreamTrackFormat = null;
+ mediaChunks.clear();
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ }
+ }
+ return selectedNewTracks;
+ }
+
+ public void seekTo(long positionUs) {
+ lastSeekPositionUs = positionUs;
+ pendingResetPositionUs = positionUs;
+ loadingFinished = false;
+ mediaChunks.clear();
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ int sampleQueueCount = sampleQueues.size();
+ for (int i = 0; i < sampleQueueCount; i++) {
+ sampleQueues.valueAt(i).reset(groupEnabledStates[i]);
+ }
+ }
+ }
+
+ public long getBufferedPositionUs() {
+ if (loadingFinished) {
+ return C.TIME_END_OF_SOURCE;
+ } else if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ long bufferedPositionUs = lastSeekPositionUs;
+ HlsMediaChunk lastMediaChunk = mediaChunks.getLast();
+ HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
+ : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
+ if (lastCompletedMediaChunk != null) {
+ bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
+ }
+ int sampleQueueCount = sampleQueues.size();
+ for (int i = 0; i < sampleQueueCount; i++) {
+ bufferedPositionUs = Math.max(bufferedPositionUs,
+ sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+ }
+ return bufferedPositionUs;
+ }
+ }
+
+ public void release() {
+ int sampleQueueCount = sampleQueues.size();
+ for (int i = 0; i < sampleQueueCount; i++) {
+ sampleQueues.valueAt(i).disable();
+ }
+ loader.release();
+ handler.removeCallbacksAndMessages(null);
+ released = true;
+ }
+
+ public void setIsTimestampMaster(boolean isTimestampMaster) {
+ chunkSource.setIsTimestampMaster(isTimestampMaster);
+ }
+
+ public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
+ chunkSource.onPlaylistBlacklisted(url, blacklistMs);
+ }
+
+ // SampleStream implementation.
+
+ /* package */ boolean isReady(int group) {
+ return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(group).isEmpty());
+ }
+
+ /* package */ void maybeThrowError() throws IOException {
+ loader.maybeThrowError();
+ chunkSource.maybeThrowError();
+ }
+
+ /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer,
+ boolean requireFormat) {
+ if (isPendingReset()) {
+ return C.RESULT_NOTHING_READ;
+ }
+
+ while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) {
+ mediaChunks.removeFirst();
+ }
+ HlsMediaChunk currentChunk = mediaChunks.getFirst();
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(downstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(trackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ downstreamTrackFormat = trackFormat;
+
+ return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat,
+ loadingFinished, lastSeekPositionUs);
+ }
+
+ /* package */ void skipData(int group, long positionUs) {
+ DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group);
+ if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
+ sampleQueue.skipAll();
+ } else {
+ sampleQueue.skipToKeyframeBefore(positionUs, true);
+ }
+ }
+
+ private boolean finishedReadingChunk(HlsMediaChunk chunk) {
+ int chunkUid = chunk.uid;
+ for (int i = 0; i < sampleQueues.size(); i++) {
+ if (groupEnabledStates[i] && sampleQueues.valueAt(i).peekSourceId() == chunkUid) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // SequenceableLoader implementation
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ if (loadingFinished || loader.isLoading()) {
+ return false;
+ }
+
+ chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(),
+ pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs,
+ nextChunkHolder);
+ boolean endOfStream = nextChunkHolder.endOfStream;
+ Chunk loadable = nextChunkHolder.chunk;
+ HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist;
+ nextChunkHolder.clear();
+
+ if (endOfStream) {
+ loadingFinished = true;
+ return true;
+ }
+
+ if (loadable == null) {
+ if (playlistToLoad != null) {
+ callback.onPlaylistRefreshRequired(playlistToLoad);
+ }
+ return false;
+ }
+
+ if (isMediaChunk(loadable)) {
+ pendingResetPositionUs = C.TIME_UNSET;
+ HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable;
+ mediaChunk.init(this);
+ mediaChunks.add(mediaChunk);
+ }
+ long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount);
+ eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+ loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+ loadable.endTimeUs, elapsedRealtimeMs);
+ return true;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ if (isPendingReset()) {
+ return pendingResetPositionUs;
+ } else {
+ return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs;
+ }
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+ chunkSource.onChunkLoadCompleted(loadable);
+ eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+ loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+ loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ } else {
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ boolean released) {
+ eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+ loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+ loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
+ if (!released) {
+ int sampleQueueCount = sampleQueues.size();
+ for (int i = 0; i < sampleQueueCount; i++) {
+ sampleQueues.valueAt(i).reset(groupEnabledStates[i]);
+ }
+ callback.onContinueLoadingRequested(this);
+ }
+ }
+
+ @Override
+ public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+ IOException error) {
+ long bytesLoaded = loadable.bytesLoaded();
+ boolean isMediaChunk = isMediaChunk(loadable);
+ boolean cancelable = !isMediaChunk || bytesLoaded == 0;
+ boolean canceled = false;
+ if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
+ if (isMediaChunk) {
+ HlsMediaChunk removed = mediaChunks.removeLast();
+ Assertions.checkState(removed == loadable);
+ if (mediaChunks.isEmpty()) {
+ pendingResetPositionUs = lastSeekPositionUs;
+ }
+ }
+ canceled = true;
+ }
+ eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+ loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+ loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error,
+ canceled);
+ if (canceled) {
+ if (!prepared) {
+ continueLoading(lastSeekPositionUs);
+ } else {
+ callback.onContinueLoadingRequested(this);
+ }
+ return Loader.DONT_RETRY;
+ } else {
+ return Loader.RETRY;
+ }
+ }
+
+ // Called by the consuming thread, but only when there is no loading thread.
+
+ /**
+ * Initializes the wrapper for loading a chunk.
+ *
+ * @param chunkUid The chunk's uid.
+ * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any
+ * samples already queued to the wrapper.
+ */
+ public void init(int chunkUid, boolean shouldSpliceIn) {
+ upstreamChunkUid = chunkUid;
+ for (int i = 0; i < sampleQueues.size(); i++) {
+ sampleQueues.valueAt(i).sourceId(chunkUid);
+ }
+ if (shouldSpliceIn) {
+ for (int i = 0; i < sampleQueues.size(); i++) {
+ sampleQueues.valueAt(i).splice();
+ }
+ }
+ }
+
+ // ExtractorOutput implementation. Called by the loading thread.
+
+ @Override
+ public DefaultTrackOutput track(int id, int type) {
+ if (sampleQueues.indexOfKey(id) >= 0) {
+ return sampleQueues.get(id);
+ }
+ DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator);
+ trackOutput.setUpstreamFormatChangeListener(this);
+ trackOutput.sourceId(upstreamChunkUid);
+ sampleQueues.put(id, trackOutput);
+ return trackOutput;
+ }
+
+ @Override
+ public void endTracks() {
+ sampleQueuesBuilt = true;
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ // Do nothing.
+ }
+
+ // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+ @Override
+ public void onUpstreamFormatChanged(Format format) {
+ handler.post(maybeFinishPrepareRunnable);
+ }
+
+ // Internal methods.
+
+ private void maybeFinishPrepare() {
+ if (released || prepared || !sampleQueuesBuilt) {
+ return;
+ }
+ int sampleQueueCount = sampleQueues.size();
+ for (int i = 0; i < sampleQueueCount; i++) {
+ if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
+ return;
+ }
+ }
+ buildTracks();
+ prepared = true;
+ callback.onPrepared();
+ }
+
+ /**
+ * Builds tracks that are exposed by this {@link HlsSampleStreamWrapper} instance, as well as
+ * internal data-structures required for operation.
+ * <p>
+ * Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each
+ * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata
+ * and caption tracks. We wish to allow the user to select between an adaptive track that spans
+ * all variants, as well as each individual variant. If multiple audio tracks are present within
+ * each variant then we wish to allow the user to select between those also.
+ * <p>
+ * To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) tracks,
+ * where N is the number of variants defined in the HLS master playlist. These consist of one
+ * adaptive track defined to span all variants and a track for each individual variant. The
+ * adaptive track is initially selected. The extractor is then prepared to discover the tracks
+ * inside of each variant stream. The two sets of tracks are then combined by this method to
+ * create a third set, which is the set exposed by this {@link HlsSampleStreamWrapper}:
+ * <ul>
+ * <li>The extractor tracks are inspected to infer a "primary" track type. If a video track is
+ * present then it is always the primary type. If not, audio is the primary type if present.
+ * Else text is the primary type if present. Else there is no primary type.</li>
+ * <li>If there is exactly one extractor track of the primary type, it's expanded into (N+1)
+ * exposed tracks, all of which correspond to the primary extractor track and each of which
+ * corresponds to a different chunk source track. Selecting one of these tracks has the effect
+ * of switching the selected track on the chunk source.</li>
+ * <li>All other extractor tracks are exposed directly. Selecting one of these tracks has the
+ * effect of selecting an extractor track, leaving the selected track on the chunk source
+ * unchanged.</li>
+ * </ul>
+ */
+ private void buildTracks() {
+ // Iterate through the extractor tracks to discover the "primary" track type, and the index
+ // of the single track of this type.
+ int primaryExtractorTrackType = PRIMARY_TYPE_NONE;
+ int primaryExtractorTrackIndex = C.INDEX_UNSET;
+ int extractorTrackCount = sampleQueues.size();
+ for (int i = 0; i < extractorTrackCount; i++) {
+ String sampleMimeType = sampleQueues.valueAt(i).getUpstreamFormat().sampleMimeType;
+ int trackType;
+ if (MimeTypes.isVideo(sampleMimeType)) {
+ trackType = PRIMARY_TYPE_VIDEO;
+ } else if (MimeTypes.isAudio(sampleMimeType)) {
+ trackType = PRIMARY_TYPE_AUDIO;
+ } else if (MimeTypes.isText(sampleMimeType)) {
+ trackType = PRIMARY_TYPE_TEXT;
+ } else {
+ trackType = PRIMARY_TYPE_NONE;
+ }
+ if (trackType > primaryExtractorTrackType) {
+ primaryExtractorTrackType = trackType;
+ primaryExtractorTrackIndex = i;
+ } else if (trackType == primaryExtractorTrackType
+ && primaryExtractorTrackIndex != C.INDEX_UNSET) {
+ // We have multiple tracks of the primary type. We only want an index if there only exists a
+ // single track of the primary type, so unset the index again.
+ primaryExtractorTrackIndex = C.INDEX_UNSET;
+ }
+ }
+
+ TrackGroup chunkSourceTrackGroup = chunkSource.getTrackGroup();
+ int chunkSourceTrackCount = chunkSourceTrackGroup.length;
+
+ // Instantiate the necessary internal data-structures.
+ primaryTrackGroupIndex = C.INDEX_UNSET;
+ groupEnabledStates = new boolean[extractorTrackCount];
+
+ // Construct the set of exposed track groups.
+ TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
+ for (int i = 0; i < extractorTrackCount; i++) {
+ Format sampleFormat = sampleQueues.valueAt(i).getUpstreamFormat();
+ if (i == primaryExtractorTrackIndex) {
+ Format[] formats = new Format[chunkSourceTrackCount];
+ for (int j = 0; j < chunkSourceTrackCount; j++) {
+ formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat);
+ }
+ trackGroups[i] = new TrackGroup(formats);
+ primaryTrackGroupIndex = i;
+ } else {
+ Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO
+ && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null;
+ trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat));
+ }
+ }
+ this.trackGroups = new TrackGroupArray(trackGroups);
+ }
+
+ /**
+ * Enables or disables a specified track group.
+ *
+ * @param group The index of the track group.
+ * @param enabledState True if the group is being enabled, or false if it's being disabled.
+ */
+ private void setTrackGroupEnabledState(int group, boolean enabledState) {
+ Assertions.checkState(groupEnabledStates[group] != enabledState);
+ groupEnabledStates[group] = enabledState;
+ enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1);
+ }
+
+ /**
+ * Derives a track format corresponding to a given container format, by combining it with sample
+ * level information obtained from the samples.
+ *
+ * @param containerFormat The container format for which the track format should be derived.
+ * @param sampleFormat A sample format from which to obtain sample level information.
+ * @return The derived track format.
+ */
+ private static Format deriveFormat(Format containerFormat, Format sampleFormat) {
+ if (containerFormat == null) {
+ return sampleFormat;
+ }
+ String codecs = null;
+ int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
+ if (sampleTrackType == C.TRACK_TYPE_AUDIO) {
+ codecs = getAudioCodecs(containerFormat.codecs);
+ } else if (sampleTrackType == C.TRACK_TYPE_VIDEO) {
+ codecs = getVideoCodecs(containerFormat.codecs);
+ }
+ return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate,
+ containerFormat.width, containerFormat.height, containerFormat.selectionFlags,
+ containerFormat.language);
+ }
+
+ private boolean isMediaChunk(Chunk chunk) {
+ return chunk instanceof HlsMediaChunk;
+ }
+
+ private boolean isPendingReset() {
+ return pendingResetPositionUs != C.TIME_UNSET;
+ }
+
+ private static String getAudioCodecs(String codecs) {
+ return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO);
+ }
+
+ private static String getVideoCodecs(String codecs) {
+ return getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO);
+ }
+
+ private static String getCodecsOfType(String codecs, int trackType) {
+ if (TextUtils.isEmpty(codecs)) {
+ return null;
+ }
+ String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
+ StringBuilder builder = new StringBuilder();
+ for (String codec : codecArray) {
+ if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
+ if (builder.length() > 0) {
+ builder.append(",");
+ }
+ builder.append(codec);
+ }
+ }
+ return builder.length() > 0 ? builder.toString() : null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Provides {@link TimestampAdjuster} instances for use during HLS playbacks.
+ */
+public final class TimestampAdjusterProvider {
+
+ // TODO: Prevent this array from growing indefinitely large by removing adjusters that are no
+ // longer required.
+ private final SparseArray<TimestampAdjuster> timestampAdjusters;
+
+ public TimestampAdjusterProvider() {
+ timestampAdjusters = new SparseArray<>();
+ }
+
+ /**
+ * Returns a {@link TimestampAdjuster} suitable for adjusting the pts timestamps contained in
+ * a chunk with a given discontinuity sequence.
+ *
+ * @param discontinuitySequence The chunk's discontinuity sequence.
+ * @return A {@link TimestampAdjuster}.
+ */
+ public TimestampAdjuster getAdjuster(int discontinuitySequence) {
+ TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);
+ if (adjuster == null) {
+ adjuster = new TimestampAdjuster(TimestampAdjuster.DO_NOT_OFFSET);
+ timestampAdjusters.put(discontinuitySequence, adjuster);
+ }
+ return adjuster;
+ }
+
+ /**
+ * Resets the provider.
+ */
+ public void reset() {
+ timestampAdjusters.clear();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java
@@ -0,0 +1,177 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A special purpose extractor for WebVTT content in HLS.
+ * <p>
+ * This extractor passes through non-empty WebVTT files untouched, however derives the correct
+ * sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp
+ * of the first cue header. Empty WebVTT files are not passed through, since it's not possible to
+ * derive a sample timestamp in this case.
+ */
+/* package */ final class WebvttExtractor implements Extractor {
+
+ private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)");
+ private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(\\d+)");
+
+ private final String language;
+ private final TimestampAdjuster timestampAdjuster;
+ private final ParsableByteArray sampleDataWrapper;
+
+ private ExtractorOutput output;
+
+ private byte[] sampleData;
+ private int sampleSize;
+
+ public WebvttExtractor(String language, TimestampAdjuster timestampAdjuster) {
+ this.language = language;
+ this.timestampAdjuster = timestampAdjuster;
+ this.sampleDataWrapper = new ParsableByteArray();
+ sampleData = new byte[1024];
+ }
+
+ // Extractor implementation.
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // This extractor is only used for the HLS use case, which should not call this method.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ // This extractor is only used for the HLS use case, which should not call this method.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ int currentFileSize = (int) input.getLength();
+
+ // Increase the size of sampleData if necessary.
+ if (sampleSize == sampleData.length) {
+ sampleData = Arrays.copyOf(sampleData,
+ (currentFileSize != C.LENGTH_UNSET ? currentFileSize : sampleData.length) * 3 / 2);
+ }
+
+ // Consume to the input.
+ int bytesRead = input.read(sampleData, sampleSize, sampleData.length - sampleSize);
+ if (bytesRead != C.RESULT_END_OF_INPUT) {
+ sampleSize += bytesRead;
+ if (currentFileSize == C.LENGTH_UNSET || sampleSize != currentFileSize) {
+ return Extractor.RESULT_CONTINUE;
+ }
+ }
+
+ // We've reached the end of the input, which corresponds to the end of the current file.
+ processSample();
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+
+ private void processSample() throws ParserException {
+ ParsableByteArray webvttData = new ParsableByteArray(sampleData);
+
+ // Validate the first line of the header.
+ try {
+ WebvttParserUtil.validateWebvttHeaderLine(webvttData);
+ } catch (SubtitleDecoderException e) {
+ throw new ParserException(e);
+ }
+
+ // Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header.
+ long vttTimestampUs = 0;
+ long tsTimestampUs = 0;
+
+ // Parse the remainder of the header looking for X-TIMESTAMP-MAP.
+ String line;
+ while (!TextUtils.isEmpty(line = webvttData.readLine())) {
+ if (line.startsWith("X-TIMESTAMP-MAP")) {
+ Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line);
+ if (!localTimestampMatcher.find()) {
+ throw new ParserException("X-TIMESTAMP-MAP doesn't contain local timestamp: " + line);
+ }
+ Matcher mediaTimestampMatcher = MEDIA_TIMESTAMP.matcher(line);
+ if (!mediaTimestampMatcher.find()) {
+ throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line);
+ }
+ vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1));
+ tsTimestampUs = TimestampAdjuster.ptsToUs(
+ Long.parseLong(mediaTimestampMatcher.group(1)));
+ }
+ }
+
+ // Find the first cue header and parse the start time.
+ Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData);
+ if (cueHeaderMatcher == null) {
+ // No cues found. Don't output a sample, but still output a corresponding track.
+ buildTrackOutput(0);
+ return;
+ }
+
+ long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));
+ long sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(
+ firstCueTimeUs + tsTimestampUs - vttTimestampUs);
+ long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;
+ // Output the track.
+ TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);
+ // Output the sample.
+ sampleDataWrapper.reset(sampleData, sampleSize);
+ trackOutput.sampleData(sampleDataWrapper, sampleSize);
+ trackOutput.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ }
+
+ private TrackOutput buildTrackOutput(long subsampleOffsetUs) {
+ TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
+ trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null,
+ Format.NO_VALUE, 0, language, null, subsampleOffsetUs));
+ output.endTracks();
+ return trackOutput;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls.playlist;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an HLS master playlist.
+ */
+public final class HlsMasterPlaylist extends HlsPlaylist {
+
+ /**
+ * Represents a url in an HLS master playlist.
+ */
+ public static final class HlsUrl {
+
+ public final String url;
+ public final Format format;
+
+ public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) {
+ Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null,
+ Format.NO_VALUE, 0, null);
+ return new HlsUrl(baseUri, format);
+ }
+
+ public HlsUrl(String url, Format format) {
+ this.url = url;
+ this.format = format;
+ }
+
+ }
+
+ public final List<HlsUrl> variants;
+ public final List<HlsUrl> audios;
+ public final List<HlsUrl> subtitles;
+
+ public final Format muxedAudioFormat;
+ public final List<Format> muxedCaptionFormats;
+
+ public HlsMasterPlaylist(String baseUri, List<HlsUrl> variants, List<HlsUrl> audios,
+ List<HlsUrl> subtitles, Format muxedAudioFormat, List<Format> muxedCaptionFormats) {
+ super(baseUri);
+ this.variants = Collections.unmodifiableList(variants);
+ this.audios = Collections.unmodifiableList(audios);
+ this.subtitles = Collections.unmodifiableList(subtitles);
+ this.muxedAudioFormat = muxedAudioFormat;
+ this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats);
+ }
+
+ public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) {
+ List<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri));
+ List<HlsUrl> emptyList = Collections.emptyList();
+ return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null,
+ Collections.<Format>emptyList());
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -0,0 +1,176 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls.playlist;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.C;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an HLS media playlist.
+ */
+public final class HlsMediaPlaylist extends HlsPlaylist {
+
+ /**
+ * Media segment reference.
+ */
+ public static final class Segment implements Comparable<Long> {
+
+ public final String url;
+ public final long durationUs;
+ public final int relativeDiscontinuitySequence;
+ public final long relativeStartTimeUs;
+ public final boolean isEncrypted;
+ public final String encryptionKeyUri;
+ public final String encryptionIV;
+ public final long byterangeOffset;
+ public final long byterangeLength;
+
+ public Segment(String uri, long byterangeOffset, long byterangeLength) {
+ this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
+ }
+
+ public Segment(String uri, long durationUs, int relativeDiscontinuitySequence,
+ long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
+ long byterangeOffset, long byterangeLength) {
+ this.url = uri;
+ this.durationUs = durationUs;
+ this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
+ this.relativeStartTimeUs = relativeStartTimeUs;
+ this.isEncrypted = isEncrypted;
+ this.encryptionKeyUri = encryptionKeyUri;
+ this.encryptionIV = encryptionIV;
+ this.byterangeOffset = byterangeOffset;
+ this.byterangeLength = byterangeLength;
+ }
+
+ @Override
+ public int compareTo(@NonNull Long relativeStartTimeUs) {
+ return this.relativeStartTimeUs > relativeStartTimeUs
+ ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
+ }
+
+ }
+
+ /**
+ * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT})
+ public @interface PlaylistType {}
+ public static final int PLAYLIST_TYPE_UNKNOWN = 0;
+ public static final int PLAYLIST_TYPE_VOD = 1;
+ public static final int PLAYLIST_TYPE_EVENT = 2;
+
+ @PlaylistType public final int playlistType;
+ public final long startOffsetUs;
+ public final long startTimeUs;
+ public final boolean hasDiscontinuitySequence;
+ public final int discontinuitySequence;
+ public final int mediaSequence;
+ public final int version;
+ public final long targetDurationUs;
+ public final boolean hasEndTag;
+ public final boolean hasProgramDateTime;
+ public final Segment initializationSegment;
+ public final List<Segment> segments;
+ public final long durationUs;
+
+ public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs,
+ long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence,
+ int mediaSequence, int version, long targetDurationUs, boolean hasEndTag,
+ boolean hasProgramDateTime, Segment initializationSegment, List<Segment> segments) {
+ super(baseUri);
+ this.playlistType = playlistType;
+ this.startTimeUs = startTimeUs;
+ this.hasDiscontinuitySequence = hasDiscontinuitySequence;
+ this.discontinuitySequence = discontinuitySequence;
+ this.mediaSequence = mediaSequence;
+ this.version = version;
+ this.targetDurationUs = targetDurationUs;
+ this.hasEndTag = hasEndTag;
+ this.hasProgramDateTime = hasProgramDateTime;
+ this.initializationSegment = initializationSegment;
+ this.segments = Collections.unmodifiableList(segments);
+ if (!segments.isEmpty()) {
+ Segment last = segments.get(segments.size() - 1);
+ durationUs = last.relativeStartTimeUs + last.durationUs;
+ } else {
+ durationUs = 0;
+ }
+ this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
+ : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;
+ }
+
+ /**
+ * Returns whether this playlist is newer than {@code other}.
+ *
+ * @param other The playlist to compare.
+ * @return Whether this playlist is newer than {@code other}.
+ */
+ public boolean isNewerThan(HlsMediaPlaylist other) {
+ if (other == null || mediaSequence > other.mediaSequence) {
+ return true;
+ }
+ if (mediaSequence < other.mediaSequence) {
+ return false;
+ }
+ // The media sequences are equal.
+ int segmentCount = segments.size();
+ int otherSegmentCount = other.segments.size();
+ return segmentCount > otherSegmentCount
+ || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag);
+ }
+
+ public long getEndTimeUs() {
+ return startTimeUs + durationUs;
+ }
+
+ /**
+ * Returns a playlist identical to this one except for the start time, the discontinuity sequence
+ * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,
+ * {@code hasDiscontinuitySequence} is set to true.
+ *
+ * @param startTimeUs The start time for the returned playlist.
+ * @param discontinuitySequence The discontinuity sequence for the returned playlist.
+ * @return The playlist.
+ */
+ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
+ return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true,
+ discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag,
+ hasProgramDateTime, initializationSegment, segments);
+ }
+
+ /**
+ * Returns a playlist identical to this one except that an end tag is added. If an end tag is
+ * already present then the playlist will return itself.
+ *
+ * @return The playlist.
+ */
+ public HlsMediaPlaylist copyWithEndTag() {
+ if (this.hasEndTag) {
+ return this;
+ }
+ return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs,
+ hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs,
+ true, hasProgramDateTime, initializationSegment, segments);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls.playlist;
+
+/**
+ * Represents an HLS playlist.
+ */
+public abstract class HlsPlaylist {
+
+ public final String baseUri;
+
+ protected HlsPlaylist(String baseUri) {
+ this.baseUri = baseUri;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -0,0 +1,453 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HLS playlists parsing logic.
+ */
+public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
+
+ private static final String PLAYLIST_HEADER = "#EXTM3U";
+
+ private static final String TAG_VERSION = "#EXT-X-VERSION";
+ private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
+ private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
+ private static final String TAG_MEDIA = "#EXT-X-MEDIA";
+ private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
+ private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
+ private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE";
+ private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME";
+ private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP";
+ private static final String TAG_MEDIA_DURATION = "#EXTINF";
+ private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE";
+ private static final String TAG_START = "#EXT-X-START";
+ private static final String TAG_ENDLIST = "#EXT-X-ENDLIST";
+ private static final String TAG_KEY = "#EXT-X-KEY";
+ private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE";
+
+ private static final String TYPE_AUDIO = "AUDIO";
+ private static final String TYPE_VIDEO = "VIDEO";
+ private static final String TYPE_SUBTITLES = "SUBTITLES";
+ private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS";
+
+ private static final String METHOD_NONE = "NONE";
+ private static final String METHOD_AES128 = "AES-128";
+
+ private static final String BOOLEAN_TRUE = "YES";
+ private static final String BOOLEAN_FALSE = "NO";
+
+ private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
+ private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
+ private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
+ private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
+ + ":(\\d+)\\b");
+ private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
+ private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE
+ + ":(.+)\\b");
+ private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ + ":(\\d+)\\b");
+ private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION
+ + ":([\\d\\.]+)\\b");
+ private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b");
+ private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE
+ + ":(\\d+(?:@\\d+)?)\\b");
+ private static final Pattern REGEX_ATTR_BYTERANGE =
+ Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\"");
+ private static final Pattern REGEX_METHOD = Pattern.compile("METHOD=(" + METHOD_NONE + "|"
+ + METHOD_AES128 + ")");
+ private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\"");
+ private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)");
+ private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO
+ + "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
+ private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
+ private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
+ private static final Pattern REGEX_INSTREAM_ID =
+ Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
+ private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
+ private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
+ private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
+
+ @Override
+ public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ Queue<String> extraLines = new LinkedList<>();
+ String line;
+ try {
+ if (!checkPlaylistHeader(reader)) {
+ throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.",
+ uri);
+ }
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ // Do nothing.
+ } else if (line.startsWith(TAG_STREAM_INF)) {
+ extraLines.add(line);
+ return parseMasterPlaylist(new LineIterator(extraLines, reader), uri.toString());
+ } else if (line.startsWith(TAG_TARGET_DURATION)
+ || line.startsWith(TAG_MEDIA_SEQUENCE)
+ || line.startsWith(TAG_MEDIA_DURATION)
+ || line.startsWith(TAG_KEY)
+ || line.startsWith(TAG_BYTERANGE)
+ || line.equals(TAG_DISCONTINUITY)
+ || line.equals(TAG_DISCONTINUITY_SEQUENCE)
+ || line.equals(TAG_ENDLIST)) {
+ extraLines.add(line);
+ return parseMediaPlaylist(new LineIterator(extraLines, reader), uri.toString());
+ } else {
+ extraLines.add(line);
+ }
+ }
+ } finally {
+ Util.closeQuietly(reader);
+ }
+ throw new ParserException("Failed to parse the playlist, could not identify any tags.");
+ }
+
+ private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {
+ int last = reader.read();
+ if (last == 0xEF) {
+ if (reader.read() != 0xBB || reader.read() != 0xBF) {
+ return false;
+ }
+ // The playlist contains a Byte Order Mark, which gets discarded.
+ last = reader.read();
+ }
+ last = skipIgnorableWhitespace(reader, true, last);
+ int playlistHeaderLength = PLAYLIST_HEADER.length();
+ for (int i = 0; i < playlistHeaderLength; i++) {
+ if (last != PLAYLIST_HEADER.charAt(i)) {
+ return false;
+ }
+ last = reader.read();
+ }
+ last = skipIgnorableWhitespace(reader, false, last);
+ return Util.isLinebreak(last);
+ }
+
+ private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)
+ throws IOException {
+ while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {
+ c = reader.read();
+ }
+ return c;
+ }
+
+ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
+ throws IOException {
+ ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();
+ ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
+ ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
+ Format muxedAudioFormat = null;
+ ArrayList<Format> muxedCaptionFormats = new ArrayList<>();
+
+ String line;
+ while (iterator.hasNext()) {
+ line = iterator.next();
+ if (line.startsWith(TAG_MEDIA)) {
+ @C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
+ String uri = parseOptionalStringAttr(line, REGEX_URI);
+ String id = parseStringAttr(line, REGEX_NAME);
+ String language = parseOptionalStringAttr(line, REGEX_LANGUAGE);
+ Format format;
+ switch (parseStringAttr(line, REGEX_TYPE)) {
+ case TYPE_AUDIO:
+ format = Format.createAudioContainerFormat(id, MimeTypes.APPLICATION_M3U8, null, null,
+ Format.NO_VALUE, Format.NO_VALUE, Format.NO_VALUE, null, selectionFlags, language);
+ if (uri == null) {
+ muxedAudioFormat = format;
+ } else {
+ audios.add(new HlsMasterPlaylist.HlsUrl(uri, format));
+ }
+ break;
+ case TYPE_SUBTITLES:
+ format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_M3U8,
+ MimeTypes.TEXT_VTT, null, Format.NO_VALUE, selectionFlags, language);
+ subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format));
+ break;
+ case TYPE_CLOSED_CAPTIONS:
+ String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID);
+ String mimeType;
+ int accessibilityChannel;
+ if (instreamId.startsWith("CC")) {
+ mimeType = MimeTypes.APPLICATION_CEA608;
+ accessibilityChannel = Integer.parseInt(instreamId.substring(2));
+ } else /* starts with SERVICE */ {
+ mimeType = MimeTypes.APPLICATION_CEA708;
+ accessibilityChannel = Integer.parseInt(instreamId.substring(7));
+ }
+ muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null,
+ Format.NO_VALUE, selectionFlags, language, accessibilityChannel));
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ } else if (line.startsWith(TAG_STREAM_INF)) {
+ int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
+ String codecs = parseOptionalStringAttr(line, REGEX_CODECS);
+ String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION);
+ int width;
+ int height;
+ if (resolutionString != null) {
+ String[] widthAndHeight = resolutionString.split("x");
+ width = Integer.parseInt(widthAndHeight[0]);
+ height = Integer.parseInt(widthAndHeight[1]);
+ if (width <= 0 || height <= 0) {
+ // Resolution string is invalid.
+ width = Format.NO_VALUE;
+ height = Format.NO_VALUE;
+ }
+ } else {
+ width = Format.NO_VALUE;
+ height = Format.NO_VALUE;
+ }
+ line = iterator.next();
+ Format format = Format.createVideoContainerFormat(Integer.toString(variants.size()),
+ MimeTypes.APPLICATION_M3U8, null, codecs, bitrate, width, height, Format.NO_VALUE, null,
+ 0);
+ variants.add(new HlsMasterPlaylist.HlsUrl(line, format));
+ }
+ }
+ return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
+ muxedCaptionFormats);
+ }
+
+ @C.SelectionFlags
+ private static int parseSelectionFlags(String line) {
+ return (parseBooleanAttribute(line, REGEX_DEFAULT, false) ? C.SELECTION_FLAG_DEFAULT : 0)
+ | (parseBooleanAttribute(line, REGEX_FORCED, false) ? C.SELECTION_FLAG_FORCED : 0)
+ | (parseBooleanAttribute(line, REGEX_AUTOSELECT, false) ? C.SELECTION_FLAG_AUTOSELECT : 0);
+ }
+
+ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
+ throws IOException {
+ @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
+ long startOffsetUs = C.TIME_UNSET;
+ int mediaSequence = 0;
+ int version = 1; // Default version == 1.
+ long targetDurationUs = C.TIME_UNSET;
+ boolean hasEndTag = false;
+ Segment initializationSegment = null;
+ List<Segment> segments = new ArrayList<>();
+
+ long segmentDurationUs = 0;
+ boolean hasDiscontinuitySequence = false;
+ int playlistDiscontinuitySequence = 0;
+ int relativeDiscontinuitySequence = 0;
+ long playlistStartTimeUs = 0;
+ long segmentStartTimeUs = 0;
+ long segmentByteRangeOffset = 0;
+ long segmentByteRangeLength = C.LENGTH_UNSET;
+ int segmentMediaSequence = 0;
+
+ boolean isEncrypted = false;
+ String encryptionKeyUri = null;
+ String encryptionIV = null;
+
+ String line;
+ while (iterator.hasNext()) {
+ line = iterator.next();
+ if (line.startsWith(TAG_PLAYLIST_TYPE)) {
+ String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE);
+ if ("VOD".equals(playlistTypeString)) {
+ playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
+ } else if ("EVENT".equals(playlistTypeString)) {
+ playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
+ } else {
+ throw new ParserException("Illegal playlist type: " + playlistTypeString);
+ }
+ } else if (line.startsWith(TAG_START)) {
+ startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
+ } else if (line.startsWith(TAG_INIT_SEGMENT)) {
+ String uri = parseStringAttr(line, REGEX_URI);
+ String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE);
+ if (byteRange != null) {
+ String[] splitByteRange = byteRange.split("@");
+ segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+ if (splitByteRange.length > 1) {
+ segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+ }
+ }
+ initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
+ segmentByteRangeOffset = 0;
+ segmentByteRangeLength = C.LENGTH_UNSET;
+ } else if (line.startsWith(TAG_TARGET_DURATION)) {
+ targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
+ } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
+ mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
+ segmentMediaSequence = mediaSequence;
+ } else if (line.startsWith(TAG_VERSION)) {
+ version = parseIntAttr(line, REGEX_VERSION);
+ } else if (line.startsWith(TAG_MEDIA_DURATION)) {
+ segmentDurationUs =
+ (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
+ } else if (line.startsWith(TAG_KEY)) {
+ String method = parseStringAttr(line, REGEX_METHOD);
+ isEncrypted = METHOD_AES128.equals(method);
+ if (isEncrypted) {
+ encryptionKeyUri = parseStringAttr(line, REGEX_URI);
+ encryptionIV = parseOptionalStringAttr(line, REGEX_IV);
+ } else {
+ encryptionKeyUri = null;
+ encryptionIV = null;
+ }
+ } else if (line.startsWith(TAG_BYTERANGE)) {
+ String byteRange = parseStringAttr(line, REGEX_BYTERANGE);
+ String[] splitByteRange = byteRange.split("@");
+ segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+ if (splitByteRange.length > 1) {
+ segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+ }
+ } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
+ hasDiscontinuitySequence = true;
+ playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
+ } else if (line.equals(TAG_DISCONTINUITY)) {
+ relativeDiscontinuitySequence++;
+ } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
+ if (playlistStartTimeUs == 0) {
+ long programDatetimeUs =
+ C.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
+ playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
+ }
+ } else if (!line.startsWith("#")) {
+ String segmentEncryptionIV;
+ if (!isEncrypted) {
+ segmentEncryptionIV = null;
+ } else if (encryptionIV != null) {
+ segmentEncryptionIV = encryptionIV;
+ } else {
+ segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
+ }
+ segmentMediaSequence++;
+ if (segmentByteRangeLength == C.LENGTH_UNSET) {
+ segmentByteRangeOffset = 0;
+ }
+ segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
+ segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
+ segmentByteRangeOffset, segmentByteRangeLength));
+ segmentStartTimeUs += segmentDurationUs;
+ segmentDurationUs = 0;
+ if (segmentByteRangeLength != C.LENGTH_UNSET) {
+ segmentByteRangeOffset += segmentByteRangeLength;
+ }
+ segmentByteRangeLength = C.LENGTH_UNSET;
+ } else if (line.equals(TAG_ENDLIST)) {
+ hasEndTag = true;
+ }
+ }
+ return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, playlistStartTimeUs,
+ hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version,
+ targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments);
+ }
+
+ private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find() && matcher.groupCount() == 1) {
+ return matcher.group(1);
+ }
+ throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
+ }
+
+ private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
+ return Integer.parseInt(parseStringAttr(line, pattern));
+ }
+
+ private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
+ return Double.parseDouble(parseStringAttr(line, pattern));
+ }
+
+ private static String parseOptionalStringAttr(String line, Pattern pattern) {
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return null;
+ }
+
+ private static boolean parseBooleanAttribute(String line, Pattern pattern, boolean defaultValue) {
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ return matcher.group(1).equals(BOOLEAN_TRUE);
+ }
+ return defaultValue;
+ }
+
+ private static Pattern compileBooleanAttrPattern(String attribute) {
+ return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")");
+ }
+
+ private static class LineIterator {
+
+ private final BufferedReader reader;
+ private final Queue<String> extraLines;
+
+ private String next;
+
+ public LineIterator(Queue<String> extraLines, BufferedReader reader) {
+ this.extraLines = extraLines;
+ this.reader = reader;
+ }
+
+ public boolean hasNext() throws IOException {
+ if (next != null) {
+ return true;
+ }
+ if (!extraLines.isEmpty()) {
+ next = extraLines.poll();
+ return true;
+ }
+ while ((next = reader.readLine()) != null) {
+ next = next.trim();
+ if (!next.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String next() throws IOException {
+ String result = null;
+ if (hasNext()) {
+ result = next;
+ next = null;
+ }
+ return result;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
@@ -0,0 +1,548 @@
+/*
+ * 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 com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.UriUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/**
+ * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS
+ * master playlist or a media playlist.
+ */
+public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
+
+ /**
+ * Listener for primary playlist changes.
+ */
+ public interface PrimaryPlaylistListener {
+
+ /**
+ * Called when the primary playlist changes.
+ *
+ * @param mediaPlaylist The primary playlist new snapshot.
+ */
+ void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
+
+ }
+
+ /**
+ * Called on playlist loading events.
+ */
+ public interface PlaylistEventListener {
+
+ /**
+ * Called a playlist changes.
+ */
+ void onPlaylistChanged();
+
+ /**
+ * Called if an error is encountered while loading a playlist.
+ *
+ * @param url The loaded url that caused the error.
+ * @param blacklistDurationMs The number of milliseconds for which the playlist has been
+ * blacklisted.
+ */
+ void onPlaylistBlacklisted(HlsUrl url, long blacklistDurationMs);
+
+ }
+
+ /**
+ * The minimum number of milliseconds that a url is kept as primary url, if no
+ * {@link #getPlaylistSnapshot} call is made for that url.
+ */
+ private static final long PRIMARY_URL_KEEPALIVE_MS = 15000;
+
+ private final Uri initialPlaylistUri;
+ private final HlsDataSourceFactory dataSourceFactory;
+ private final HlsPlaylistParser playlistParser;
+ private final int minRetryCount;
+ private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
+ private final Handler playlistRefreshHandler;
+ private final PrimaryPlaylistListener primaryPlaylistListener;
+ private final List<PlaylistEventListener> listeners;
+ private final Loader initialPlaylistLoader;
+ private final EventDispatcher eventDispatcher;
+
+ private HlsMasterPlaylist masterPlaylist;
+ private HlsUrl primaryHlsUrl;
+ private HlsMediaPlaylist primaryUrlSnapshot;
+ private boolean isLive;
+
+ /**
+ * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media
+ * playlist or a master playlist.
+ * @param dataSourceFactory A factory for {@link DataSource} instances.
+ * @param eventDispatcher A dispatcher to notify of events.
+ * @param minRetryCount The minimum number of times the load must be retried before blacklisting a
+ * playlist.
+ * @param primaryPlaylistListener A callback for the primary playlist change events.
+ */
+ public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory,
+ EventDispatcher eventDispatcher, int minRetryCount,
+ PrimaryPlaylistListener primaryPlaylistListener) {
+ this.initialPlaylistUri = initialPlaylistUri;
+ this.dataSourceFactory = dataSourceFactory;
+ this.eventDispatcher = eventDispatcher;
+ this.minRetryCount = minRetryCount;
+ this.primaryPlaylistListener = primaryPlaylistListener;
+ listeners = new ArrayList<>();
+ initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
+ playlistParser = new HlsPlaylistParser();
+ playlistBundles = new IdentityHashMap<>();
+ playlistRefreshHandler = new Handler();
+ }
+
+ /**
+ * Registers a listener to receive events from the playlist tracker.
+ *
+ * @param listener The listener.
+ */
+ public void addListener(PlaylistEventListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param listener The listener to unregister.
+ */
+ public void removeListener(PlaylistEventListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Starts tracking all the playlists related to the provided Uri.
+ */
+ public void start() {
+ ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = new ParsingLoadable<>(
+ dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri,
+ C.DATA_TYPE_MANIFEST, playlistParser);
+ initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
+ }
+
+ /**
+ * Returns the master playlist.
+ *
+ * @return The master playlist. Null if the initial playlist has yet to be loaded.
+ */
+ public HlsMasterPlaylist getMasterPlaylist() {
+ return masterPlaylist;
+ }
+
+ /**
+ * Returns the most recent snapshot available of the playlist referenced by the provided
+ * {@link HlsUrl}.
+ *
+ * @param url The {@link HlsUrl} corresponding to the requested media playlist.
+ * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May
+ * be null if no snapshot has been loaded yet.
+ */
+ public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
+ HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
+ if (snapshot != null) {
+ maybeSetPrimaryUrl(url);
+ }
+ return snapshot;
+ }
+
+ /**
+ * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
+ * valid, meaning all the segments referenced by the playlist are expected to be available. If the
+ * playlist is not valid then some of the segments may no longer be available.
+
+ * @param url The {@link HlsUrl}.
+ * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
+ * valid.
+ */
+ public boolean isSnapshotValid(HlsUrl url) {
+ return playlistBundles.get(url).isSnapshotValid();
+ }
+
+ /**
+ * Releases the playlist tracker.
+ */
+ public void release() {
+ initialPlaylistLoader.release();
+ for (MediaPlaylistBundle bundle : playlistBundles.values()) {
+ bundle.release();
+ }
+ playlistRefreshHandler.removeCallbacksAndMessages(null);
+ playlistBundles.clear();
+ }
+
+ /**
+ * If the tracker is having trouble refreshing the primary playlist or loading an irreplaceable
+ * playlist, this method throws the underlying error. Otherwise, does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ public void maybeThrowPlaylistRefreshError() throws IOException {
+ initialPlaylistLoader.maybeThrowError();
+ if (primaryHlsUrl != null) {
+ playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError();
+ }
+ }
+
+ /**
+ * Triggers a playlist refresh and whitelists it.
+ *
+ * @param url The {@link HlsUrl} of the playlist to be refreshed.
+ */
+ public void refreshPlaylist(HlsUrl url) {
+ playlistBundles.get(url).loadPlaylist();
+ }
+
+ /**
+ * Returns whether this is live content.
+ *
+ * @return True if the content is live. False otherwise.
+ */
+ public boolean isLive() {
+ return isLive;
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+ long loadDurationMs) {
+ HlsPlaylist result = loadable.getResult();
+ HlsMasterPlaylist masterPlaylist;
+ boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
+ if (isMediaPlaylist) {
+ masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri);
+ } else /* result instanceof HlsMasterPlaylist */ {
+ masterPlaylist = (HlsMasterPlaylist) result;
+ }
+ this.masterPlaylist = masterPlaylist;
+ primaryHlsUrl = masterPlaylist.variants.get(0);
+ ArrayList<HlsUrl> urls = new ArrayList<>();
+ urls.addAll(masterPlaylist.variants);
+ urls.addAll(masterPlaylist.audios);
+ urls.addAll(masterPlaylist.subtitles);
+ createBundles(urls);
+ MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl);
+ if (isMediaPlaylist) {
+ // We don't need to load the playlist again. We can use the same result.
+ primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result);
+ } else {
+ primaryBundle.loadPlaylist();
+ }
+ eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+ loadDurationMs, loadable.bytesLoaded());
+ }
+
+ @Override
+ public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+ long loadDurationMs, boolean released) {
+ eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+ loadDurationMs, loadable.bytesLoaded());
+ }
+
+ @Override
+ public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+ long loadDurationMs, IOException error) {
+ boolean isFatal = error instanceof ParserException;
+ eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+ loadDurationMs, loadable.bytesLoaded(), error, isFatal);
+ return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
+ }
+
+ // Internal methods.
+
+ private boolean maybeSelectNewPrimaryUrl() {
+ List<HlsUrl> variants = masterPlaylist.variants;
+ int variantsSize = variants.size();
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ for (int i = 0; i < variantsSize; i++) {
+ MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
+ if (currentTimeMs > bundle.blacklistUntilMs) {
+ primaryHlsUrl = bundle.playlistUrl;
+ bundle.loadPlaylist();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void maybeSetPrimaryUrl(HlsUrl url) {
+ if (!masterPlaylist.variants.contains(url)
+ || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) {
+ // Only allow variant urls to be chosen as primary. Also prevent changing the primary url if
+ // the last primary snapshot contains an end tag.
+ return;
+ }
+ MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl);
+ long primarySnapshotAccessAgeMs =
+ currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime();
+ if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) {
+ primaryHlsUrl = url;
+ playlistBundles.get(primaryHlsUrl).loadPlaylist();
+ }
+ }
+
+ private void createBundles(List<HlsUrl> urls) {
+ int listSize = urls.size();
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ for (int i = 0; i < listSize; i++) {
+ HlsUrl url = urls.get(i);
+ MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs);
+ playlistBundles.put(url, bundle);
+ }
+ }
+
+ /**
+ * Called by the bundles when a snapshot changes.
+ *
+ * @param url The url of the playlist.
+ * @param newSnapshot The new snapshot.
+ * @return True if a refresh should be scheduled.
+ */
+ private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
+ if (url == primaryHlsUrl) {
+ if (primaryUrlSnapshot == null) {
+ // This is the first primary url snapshot.
+ isLive = !newSnapshot.hasEndTag;
+ }
+ primaryUrlSnapshot = newSnapshot;
+ primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
+ }
+ int listenersSize = listeners.size();
+ for (int i = 0; i < listenersSize; i++) {
+ listeners.get(i).onPlaylistChanged();
+ }
+ // If the primary playlist is not the final one, we should schedule a refresh.
+ return url == primaryHlsUrl && !newSnapshot.hasEndTag;
+ }
+
+ private void notifyPlaylistBlacklisting(HlsUrl url, long blacklistMs) {
+ int listenersSize = listeners.size();
+ for (int i = 0; i < listenersSize; i++) {
+ listeners.get(i).onPlaylistBlacklisted(url, blacklistMs);
+ }
+ }
+
+ private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist,
+ HlsMediaPlaylist loadedPlaylist) {
+ if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
+ if (loadedPlaylist.hasEndTag) {
+ // If the loaded playlist has an end tag but is not newer than the old playlist then we have
+ // an inconsistent state. This is typically caused by the server incorrectly resetting the
+ // media sequence when appending the end tag. We resolve this case as best we can by
+ // returning the old playlist with the end tag appended.
+ return oldPlaylist.copyWithEndTag();
+ } else {
+ return oldPlaylist;
+ }
+ }
+ long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
+ int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
+ return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
+ }
+
+ private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist,
+ HlsMediaPlaylist loadedPlaylist) {
+ if (loadedPlaylist.hasProgramDateTime) {
+ return loadedPlaylist.startTimeUs;
+ }
+ long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
+ ? primaryUrlSnapshot.startTimeUs : 0;
+ if (oldPlaylist == null) {
+ return primarySnapshotStartTimeUs;
+ }
+ int oldPlaylistSize = oldPlaylist.segments.size();
+ Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
+ if (firstOldOverlappingSegment != null) {
+ return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
+ } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
+ return oldPlaylist.getEndTimeUs();
+ } else {
+ // No segments overlap, we assume the new playlist start coincides with the primary playlist.
+ return primarySnapshotStartTimeUs;
+ }
+ }
+
+ private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist,
+ HlsMediaPlaylist loadedPlaylist) {
+ if (loadedPlaylist.hasDiscontinuitySequence) {
+ return loadedPlaylist.discontinuitySequence;
+ }
+ // TODO: Improve cross-playlist discontinuity adjustment.
+ int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null
+ ? primaryUrlSnapshot.discontinuitySequence : 0;
+ if (oldPlaylist == null) {
+ return primaryUrlDiscontinuitySequence;
+ }
+ Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
+ if (firstOldOverlappingSegment != null) {
+ return oldPlaylist.discontinuitySequence
+ + firstOldOverlappingSegment.relativeDiscontinuitySequence
+ - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
+ }
+ return primaryUrlDiscontinuitySequence;
+ }
+
+ private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist,
+ HlsMediaPlaylist loadedPlaylist) {
+ int mediaSequenceOffset = loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence;
+ List<Segment> oldSegments = oldPlaylist.segments;
+ return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
+ }
+
+ /**
+ * Holds all information related to a specific Media Playlist.
+ */
+ private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>>,
+ Runnable {
+
+ private final HlsUrl playlistUrl;
+ private final Loader mediaPlaylistLoader;
+ private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
+
+ private HlsMediaPlaylist playlistSnapshot;
+ private long lastSnapshotLoadMs;
+ private long lastSnapshotAccessTimeMs;
+ private long blacklistUntilMs;
+ private boolean pendingRefresh;
+
+ public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) {
+ this.playlistUrl = playlistUrl;
+ lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs;
+ mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist");
+ mediaPlaylistLoadable = new ParsingLoadable<>(
+ dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
+ UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST,
+ playlistParser);
+ }
+
+ public HlsMediaPlaylist getPlaylistSnapshot() {
+ lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime();
+ return playlistSnapshot;
+ }
+
+ public boolean isSnapshotValid() {
+ if (playlistSnapshot == null) {
+ return false;
+ }
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));
+ return playlistSnapshot.hasEndTag
+ || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
+ || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
+ || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;
+ }
+
+ public void release() {
+ mediaPlaylistLoader.release();
+ }
+
+ public void loadPlaylist() {
+ blacklistUntilMs = 0;
+ if (!pendingRefresh && !mediaPlaylistLoader.isLoading()) {
+ mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
+ }
+ }
+
+ // Loader.Callback implementation.
+
+ @Override
+ public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+ long loadDurationMs) {
+ HlsPlaylist result = loadable.getResult();
+ if (result instanceof HlsMediaPlaylist) {
+ processLoadedPlaylist((HlsMediaPlaylist) result);
+ eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+ loadDurationMs, loadable.bytesLoaded());
+ } else {
+ onLoadError(loadable, elapsedRealtimeMs, loadDurationMs,
+ new ParserException("Loaded playlist has unexpected type."));
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+ long loadDurationMs, boolean released) {
+ eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+ loadDurationMs, loadable.bytesLoaded());
+ }
+
+ @Override
+ public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+ long loadDurationMs, IOException error) {
+ boolean isFatal = error instanceof ParserException;
+ eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+ loadDurationMs, loadable.bytesLoaded(), error, isFatal);
+ if (isFatal) {
+ return Loader.DONT_RETRY_FATAL;
+ }
+ boolean shouldRetry = true;
+ if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) {
+ blacklistUntilMs =
+ SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
+ notifyPlaylistBlacklisting(playlistUrl,
+ ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
+ shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
+ }
+ return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY;
+ }
+
+ // Runnable implementation.
+
+ @Override
+ public void run() {
+ pendingRefresh = false;
+ loadPlaylist();
+ }
+
+ // Internal methods.
+
+ private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
+ HlsMediaPlaylist oldPlaylist = playlistSnapshot;
+ lastSnapshotLoadMs = SystemClock.elapsedRealtime();
+ playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
+ long refreshDelayUs = C.TIME_UNSET;
+ if (playlistSnapshot != oldPlaylist) {
+ if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) {
+ refreshDelayUs = playlistSnapshot.targetDurationUs;
+ }
+ } else if (!playlistSnapshot.hasEndTag) {
+ refreshDelayUs = playlistSnapshot.targetDurationUs / 2;
+ }
+ if (refreshDelayUs != C.TIME_UNSET) {
+ // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing.
+ pendingRefresh = playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs));
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java
@@ -0,0 +1,167 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import android.annotation.TargetApi;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.support.annotation.IntDef;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A compatibility wrapper for {@link CaptionStyle}.
+ */
+public final class CaptionStyleCompat {
+
+ /**
+ * The type of edge, which may be none.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({EDGE_TYPE_NONE, EDGE_TYPE_OUTLINE, EDGE_TYPE_DROP_SHADOW, EDGE_TYPE_RAISED,
+ EDGE_TYPE_DEPRESSED})
+ public @interface EdgeType {}
+ /**
+ * Edge type value specifying no character edges.
+ */
+ public static final int EDGE_TYPE_NONE = 0;
+ /**
+ * Edge type value specifying uniformly outlined character edges.
+ */
+ public static final int EDGE_TYPE_OUTLINE = 1;
+ /**
+ * Edge type value specifying drop-shadowed character edges.
+ */
+ public static final int EDGE_TYPE_DROP_SHADOW = 2;
+ /**
+ * Edge type value specifying raised bevel character edges.
+ */
+ public static final int EDGE_TYPE_RAISED = 3;
+ /**
+ * Edge type value specifying depressed bevel character edges.
+ */
+ public static final int EDGE_TYPE_DEPRESSED = 4;
+
+ /**
+ * Use color setting specified by the track and fallback to default caption style.
+ */
+ public static final int USE_TRACK_COLOR_SETTINGS = 1;
+
+ /**
+ * Default caption style.
+ */
+ public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat(
+ Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null);
+
+ /**
+ * The preferred foreground color.
+ */
+ public final int foregroundColor;
+
+ /**
+ * The preferred background color.
+ */
+ public final int backgroundColor;
+
+ /**
+ * The preferred window color.
+ */
+ public final int windowColor;
+
+ /**
+ * The preferred edge type. One of:
+ * <ul>
+ * <li>{@link #EDGE_TYPE_NONE}
+ * <li>{@link #EDGE_TYPE_OUTLINE}
+ * <li>{@link #EDGE_TYPE_DROP_SHADOW}
+ * <li>{@link #EDGE_TYPE_RAISED}
+ * <li>{@link #EDGE_TYPE_DEPRESSED}
+ * </ul>
+ */
+ @EdgeType public final int edgeType;
+
+ /**
+ * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}.
+ */
+ public final int edgeColor;
+
+ /**
+ * The preferred typeface.
+ */
+ public final Typeface typeface;
+
+ /**
+ * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}.
+ *
+ * @param captionStyle A {@link CaptionStyle}.
+ * @return The equivalent {@link CaptionStyleCompat}.
+ */
+ @TargetApi(19)
+ public static CaptionStyleCompat createFromCaptionStyle(
+ CaptioningManager.CaptionStyle captionStyle) {
+ if (Util.SDK_INT >= 21) {
+ return createFromCaptionStyleV21(captionStyle);
+ } else {
+ // Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did
+ // not exist in earlier API levels).
+ return createFromCaptionStyleV19(captionStyle);
+ }
+ }
+
+ /**
+ * @param foregroundColor See {@link #foregroundColor}.
+ * @param backgroundColor See {@link #backgroundColor}.
+ * @param windowColor See {@link #windowColor}.
+ * @param edgeType See {@link #edgeType}.
+ * @param edgeColor See {@link #edgeColor}.
+ * @param typeface See {@link #typeface}.
+ */
+ public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor,
+ @EdgeType int edgeType, int edgeColor, Typeface typeface) {
+ this.foregroundColor = foregroundColor;
+ this.backgroundColor = backgroundColor;
+ this.windowColor = windowColor;
+ this.edgeType = edgeType;
+ this.edgeColor = edgeColor;
+ this.typeface = typeface;
+ }
+
+ @TargetApi(19)
+ @SuppressWarnings("ResourceType")
+ private static CaptionStyleCompat createFromCaptionStyleV19(
+ CaptioningManager.CaptionStyle captionStyle) {
+ return new CaptionStyleCompat(
+ captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT,
+ captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface());
+ }
+
+ @TargetApi(21)
+ @SuppressWarnings("ResourceType")
+ private static CaptionStyleCompat createFromCaptionStyleV21(
+ CaptioningManager.CaptionStyle captionStyle) {
+ return new CaptionStyleCompat(
+ captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor,
+ captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor,
+ captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor,
+ captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType,
+ captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor,
+ captionStyle.getTypeface());
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/Cue.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.support.annotation.IntDef;
+import android.text.Layout.Alignment;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Contains information about a specific cue, including textual content and formatting data.
+ */
+public class Cue {
+
+ /**
+ * An unset position or width.
+ */
+ public static final float DIMEN_UNSET = Float.MIN_VALUE;
+
+ /**
+ * The type of anchor, which may be unset.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END})
+ public @interface AnchorType {}
+
+ /**
+ * An unset anchor or line type value.
+ */
+ public static final int TYPE_UNSET = Integer.MIN_VALUE;
+
+ /**
+ * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue
+ * box.
+ */
+ public static final int ANCHOR_TYPE_START = 0;
+
+ /**
+ * Anchors the middle of the cue box.
+ */
+ public static final int ANCHOR_TYPE_MIDDLE = 1;
+
+ /**
+ * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue
+ * box.
+ */
+ public static final int ANCHOR_TYPE_END = 2;
+
+ /**
+ * The type of line, which may be unset.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER})
+ public @interface LineType {}
+
+ /**
+ * Value for {@link #lineType} when {@link #line} is a fractional position.
+ */
+ public static final int LINE_TYPE_FRACTION = 0;
+
+ /**
+ * Value for {@link #lineType} when {@link #line} is a line number.
+ */
+ public static final int LINE_TYPE_NUMBER = 1;
+
+ /**
+ * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated
+ * with styling spans.
+ */
+ public final CharSequence text;
+
+ /**
+ * The alignment of the cue text within the cue box, or null if the alignment is undefined.
+ */
+ public final Alignment textAlignment;
+
+ /**
+ * The cue image, or null if this is a text cue.
+ */
+ public final Bitmap bitmap;
+
+ /**
+ * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction
+ * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of
+ * the value depends on the value of {@link #lineType}.
+ * <p>
+ * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the
+ * fractional vertical position relative to the top of the viewport.
+ */
+ public final float line;
+
+ /**
+ * The type of the {@link #line} value.
+ * <p>
+ * {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
+ * viewport.
+ * <p>
+ * {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
+ * line is taken to be the size of the first line of the cue. When {@link #line} is greater than
+ * or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from
+ * the start edge. When {@link #line} is negative lines count from the end of the viewport, with
+ * -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height
+ * of the first line of the cue, and the start and end of the viewport are the top and bottom
+ * respectively.
+ * <p>
+ * Note that it's particularly important to consider the effect of {@link #lineAnchor} when using
+ * {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a
+ * (potentially multi-line) cue at the very top of the viewport.
+ * {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue
+ * at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)}
+ * and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of
+ * the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only
+ * the last line is visible at the top of the viewport.
+ * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first
+ * line is visible at the bottom of the viewport.
+ */
+ @LineType public final int lineType;
+
+ /**
+ * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
+ * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ * <p>
+ * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
+ * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
+ * respectively.
+ */
+ @AnchorType public final int lineAnchor;
+
+ /**
+ * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
+ * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}.
+ * <p>
+ * For horizontal text, this is the horizontal position relative to the left of the viewport. Note
+ * that positioning is relative to the left of the viewport even in the case of right-to-left
+ * text.
+ */
+ public final float position;
+
+ /**
+ * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
+ * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ * <p>
+ * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
+ * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
+ * respectively.
+ */
+ @AnchorType public final int positionAnchor;
+
+ /**
+ * The size of the cue box in the writing direction specified as a fraction of the viewport size
+ * in that direction, or {@link #DIMEN_UNSET}.
+ */
+ public final float size;
+
+ /**
+ * The bitmap height as a fraction of the of the viewport size, or {@link #DIMEN_UNSET} if the
+ * bitmap should be displayed at its natural height given the bitmap dimensions and the specified
+ * {@link #size}.
+ */
+ public final float bitmapHeight;
+
+ /**
+ * Specifies whether or not the {@link #windowColor} property is set.
+ */
+ public final boolean windowColorSet;
+
+ /**
+ * The fill color of the window.
+ */
+ public final int windowColor;
+
+ /**
+ * Creates an image cue.
+ *
+ * @param bitmap See {@link #bitmap}.
+ * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed
+ * as a fraction of the viewport width.
+ * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START},
+ * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a
+ * fraction of the viewport height.
+ * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START},
+ * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ * @param width The width of the cue as a fraction of the viewport width.
+ * @param height The height of the cue as a fraction of the viewport height, or
+ * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the
+ * specified {@code width}.
+ */
+ public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor,
+ float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) {
+ this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor,
+ horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK);
+ }
+
+ /**
+ * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to
+ * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}.
+ *
+ * @param text See {@link #text}.
+ */
+ public Cue(CharSequence text) {
+ this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
+ }
+
+ /**
+ * Creates a text cue.
+ *
+ * @param text See {@link #text}.
+ * @param textAlignment See {@link #textAlignment}.
+ * @param line See {@link #line}.
+ * @param lineType See {@link #lineType}.
+ * @param lineAnchor See {@link #lineAnchor}.
+ * @param position See {@link #position}.
+ * @param positionAnchor See {@link #positionAnchor}.
+ * @param size See {@link #size}.
+ */
+ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
+ @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) {
+ this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false,
+ Color.BLACK);
+ }
+
+ /**
+ * Creates a text cue.
+ *
+ * @param text See {@link #text}.
+ * @param textAlignment See {@link #textAlignment}.
+ * @param line See {@link #line}.
+ * @param lineType See {@link #lineType}.
+ * @param lineAnchor See {@link #lineAnchor}.
+ * @param position See {@link #position}.
+ * @param positionAnchor See {@link #positionAnchor}.
+ * @param size See {@link #size}.
+ * @param windowColorSet See {@link #windowColorSet}.
+ * @param windowColor See {@link #windowColor}.
+ */
+ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
+ @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
+ boolean windowColorSet, int windowColor) {
+ this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size,
+ DIMEN_UNSET, windowColorSet, windowColor);
+ }
+
+ private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line,
+ @LineType int lineType, @AnchorType int lineAnchor, float position,
+ @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet,
+ int windowColor) {
+ this.text = text;
+ this.textAlignment = textAlignment;
+ this.bitmap = bitmap;
+ this.line = line;
+ this.lineType = lineType;
+ this.lineAnchor = lineAnchor;
+ this.position = position;
+ this.positionAnchor = positionAnchor;
+ this.size = size;
+ this.bitmapHeight = bitmapHeight;
+ this.windowColorSet = windowColorSet;
+ this.windowColor = windowColor;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
@@ -0,0 +1,92 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
+import java.nio.ByteBuffer;
+
+/**
+ * Base class for subtitle parsers that use their own decode thread.
+ */
+public abstract class SimpleSubtitleDecoder extends
+ SimpleDecoder<SubtitleInputBuffer, SubtitleOutputBuffer, SubtitleDecoderException> implements
+ SubtitleDecoder {
+
+ private final String name;
+
+ /**
+ * @param name The name of the decoder.
+ */
+ protected SimpleSubtitleDecoder(String name) {
+ super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]);
+ this.name = name;
+ setInitialInputBufferSize(1024);
+ }
+
+ @Override
+ public final String getName() {
+ return name;
+ }
+
+ @Override
+ public void setPositionUs(long timeUs) {
+ // Do nothing
+ }
+
+ @Override
+ protected final SubtitleInputBuffer createInputBuffer() {
+ return new SubtitleInputBuffer();
+ }
+
+ @Override
+ protected final SubtitleOutputBuffer createOutputBuffer() {
+ return new SimpleSubtitleOutputBuffer(this);
+ }
+
+ @Override
+ protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) {
+ super.releaseOutputBuffer(buffer);
+ }
+
+ @Override
+ protected final SubtitleDecoderException decode(SubtitleInputBuffer inputBuffer,
+ SubtitleOutputBuffer outputBuffer, boolean reset) {
+ try {
+ ByteBuffer inputData = inputBuffer.data;
+ Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset);
+ outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs);
+ // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]).
+ outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY);
+ return null;
+ } catch (SubtitleDecoderException e) {
+ return e;
+ }
+ }
+
+ /**
+ * Decodes data into a {@link Subtitle}.
+ *
+ * @param data An array holding the data to be decoded, starting at position 0.
+ * @param size The size of the data to be decoded.
+ * @param reset Whether the decoder must be reset before decoding.
+ * @return The decoded {@link Subtitle}.
+ * @throws SubtitleDecoderException If a decoding error occurs.
+ */
+ protected abstract Subtitle decode(byte[] data, int size, boolean reset)
+ throws SubtitleDecoderException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+/**
+ * A {@link SubtitleOutputBuffer} for decoders that extend {@link SimpleSubtitleDecoder}.
+ */
+/* package */ final class SimpleSubtitleOutputBuffer extends SubtitleOutputBuffer {
+
+ private final SimpleSubtitleDecoder owner;
+
+ /**
+ * @param owner The decoder that owns this buffer.
+ */
+ public SimpleSubtitleOutputBuffer(SimpleSubtitleDecoder owner) {
+ super();
+ this.owner = owner;
+ }
+
+ @Override
+ public final void release() {
+ owner.releaseOutputBuffer(this);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/Subtitle.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.C;
+import java.util.List;
+
+/**
+ * A subtitle consisting of timed {@link Cue}s.
+ */
+public interface Subtitle {
+
+ /**
+ * Returns the index of the first event that occurs after a given time (exclusive).
+ *
+ * @param timeUs The time in microseconds.
+ * @return The index of the next event, or {@link C#INDEX_UNSET} if there are no events after the
+ * specified time.
+ */
+ int getNextEventTimeIndex(long timeUs);
+
+ /**
+ * Returns the number of event times, where events are defined as points in time at which the cues
+ * returned by {@link #getCues(long)} changes.
+ *
+ * @return The number of event times.
+ */
+ int getEventTimeCount();
+
+ /**
+ * Returns the event time at a specified index.
+ *
+ * @param index The index of the event time to obtain.
+ * @return The event time in microseconds.
+ */
+ long getEventTime(int index);
+
+ /**
+ * Retrieve the cues that should be displayed at a given time.
+ *
+ * @param timeUs The time in microseconds.
+ * @return A list of cues that should be displayed, possibly empty.
+ */
+ List<Cue> getCues(long timeUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SubtitleDecoder.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.decoder.Decoder;
+
+/**
+ * Decodes {@link Subtitle}s from {@link SubtitleInputBuffer}s.
+ */
+public interface SubtitleDecoder extends
+ Decoder<SubtitleInputBuffer, SubtitleOutputBuffer, SubtitleDecoderException> {
+
+ /**
+ * Informs the decoder of the current playback position.
+ * <p>
+ * Must be called prior to each attempt to dequeue output buffers from the decoder.
+ *
+ * @param positionUs The current playback position in microseconds.
+ */
+ void setPositionUs(long positionUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SubtitleDecoderException.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+/**
+ * Thrown when an error occurs decoding subtitle data.
+ */
+public class SubtitleDecoderException extends Exception {
+
+ /**
+ * @param message The detail message for this exception.
+ */
+ public SubtitleDecoderException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message The detail message for this exception.
+ * @param cause The cause of this exception.
+ */
+ public SubtitleDecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
@@ -0,0 +1,110 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.cea.Cea608Decoder;
+import com.google.android.exoplayer2.text.cea.Cea708Decoder;
+import com.google.android.exoplayer2.text.dvb.DvbDecoder;
+import com.google.android.exoplayer2.text.subrip.SubripDecoder;
+import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
+import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
+import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;
+import com.google.android.exoplayer2.text.webvtt.WebvttDecoder;
+import com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * A factory for {@link SubtitleDecoder} instances.
+ */
+public interface SubtitleDecoderFactory {
+
+ /**
+ * Returns whether the factory is able to instantiate a {@link SubtitleDecoder} for the given
+ * {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return Whether the factory can instantiate a suitable {@link SubtitleDecoder}.
+ */
+ boolean supportsFormat(Format format);
+
+ /**
+ * Creates a {@link SubtitleDecoder} for the given {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return A new {@link SubtitleDecoder}.
+ * @throws IllegalArgumentException If the {@link Format} is not supported.
+ */
+ SubtitleDecoder createDecoder(Format format);
+
+ /**
+ * Default {@link SubtitleDecoderFactory} implementation.
+ * <p>
+ * The formats supported by this factory are:
+ * <ul>
+ * <li>WebVTT ({@link WebvttDecoder})</li>
+ * <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})</li>
+ * <li>TTML ({@link TtmlDecoder})</li>
+ * <li>SubRip ({@link SubripDecoder})</li>
+ * <li>TX3G ({@link Tx3gDecoder})</li>
+ * <li>Cea608 ({@link Cea608Decoder})</li>
+ * <li>Cea708 ({@link Cea708Decoder})</li>
+ * <li>DVB ({@link DvbDecoder})</li>
+ * </ul>
+ */
+ SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() {
+
+ @Override
+ public boolean supportsFormat(Format format) {
+ String mimeType = format.sampleMimeType;
+ return MimeTypes.TEXT_VTT.equals(mimeType)
+ || MimeTypes.APPLICATION_TTML.equals(mimeType)
+ || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
+ || MimeTypes.APPLICATION_SUBRIP.equals(mimeType)
+ || MimeTypes.APPLICATION_TX3G.equals(mimeType)
+ || MimeTypes.APPLICATION_CEA608.equals(mimeType)
+ || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(mimeType)
+ || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType);
+ }
+
+ @Override
+ public SubtitleDecoder createDecoder(Format format) {
+ switch (format.sampleMimeType) {
+ case MimeTypes.TEXT_VTT:
+ return new WebvttDecoder();
+ case MimeTypes.APPLICATION_MP4VTT:
+ return new Mp4WebvttDecoder();
+ case MimeTypes.APPLICATION_TTML:
+ return new TtmlDecoder();
+ case MimeTypes.APPLICATION_SUBRIP:
+ return new SubripDecoder();
+ case MimeTypes.APPLICATION_TX3G:
+ return new Tx3gDecoder(format.initializationData);
+ case MimeTypes.APPLICATION_CEA608:
+ case MimeTypes.APPLICATION_MP4CEA608:
+ return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel);
+ case MimeTypes.APPLICATION_CEA708:
+ return new Cea708Decoder(format.accessibilityChannel);
+ case MimeTypes.APPLICATION_DVBSUBS:
+ return new DvbDecoder(format.initializationData);
+ default:
+ throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
+ }
+ }
+
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.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 com.google.android.exoplayer2.text;
+
+import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+
+/**
+ * A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}.
+ */
+public final class SubtitleInputBuffer extends DecoderInputBuffer
+ implements Comparable<SubtitleInputBuffer> {
+
+ /**
+ * An offset that must be added to the subtitle's event times after it's been decoded, or
+ * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.
+ */
+ public long subsampleOffsetUs;
+
+ public SubtitleInputBuffer() {
+ super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+ @Override
+ public int compareTo(@NonNull SubtitleInputBuffer other) {
+ long delta = timeUs - other.timeUs;
+ if (delta == 0) {
+ return 0;
+ }
+ return delta > 0 ? 1 : -1;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.OutputBuffer;
+import java.util.List;
+
+/**
+ * Base class for {@link SubtitleDecoder} output buffers.
+ */
+public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle {
+
+ private Subtitle subtitle;
+ private long subsampleOffsetUs;
+
+ /**
+ * Sets the content of the output buffer, consisting of a {@link Subtitle} and associated
+ * metadata.
+ *
+ * @param timeUs The time of the start of the subtitle in microseconds.
+ * @param subtitle The subtitle.
+ * @param subsampleOffsetUs An offset that must be added to the subtitle's event times, or
+ * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@code timeUs} should be added.
+ */
+ public void setContent(long timeUs, Subtitle subtitle, long subsampleOffsetUs) {
+ this.timeUs = timeUs;
+ this.subtitle = subtitle;
+ this.subsampleOffsetUs = subsampleOffsetUs == Format.OFFSET_SAMPLE_RELATIVE ? this.timeUs
+ : subsampleOffsetUs;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return subtitle.getEventTimeCount();
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return subtitle.getEventTime(index) + subsampleOffsetUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs);
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return subtitle.getCues(timeUs - subsampleOffsetUs);
+ }
+
+ @Override
+ public abstract void release();
+
+ @Override
+ public void clear() {
+ super.clear();
+ subtitle = null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/TextRenderer.java
@@ -0,0 +1,331 @@
+/*
+ * 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 com.google.android.exoplayer2.text;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A renderer for text.
+ * <p>
+ * {@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances obtained
+ * from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s is
+ * delegated to an {@link Output}.
+ */
+public final class TextRenderer extends BaseRenderer implements Callback {
+
+ /**
+ * Receives output from a {@link TextRenderer}.
+ */
+ public interface Output {
+
+ /**
+ * Called each time there is a change in the {@link Cue}s.
+ *
+ * @param cues The {@link Cue}s.
+ */
+ void onCues(List<Cue> cues);
+
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REPLACEMENT_STATE_NONE, REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
+ REPLACEMENT_STATE_WAIT_END_OF_STREAM})
+ private @interface ReplacementState {}
+ /**
+ * The decoder does not need to be replaced.
+ */
+ private static final int REPLACEMENT_STATE_NONE = 0;
+ /**
+ * The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
+ * decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
+ * release it.
+ */
+ private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;
+ /**
+ * The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
+ * We're waiting for the decoder to output an end of stream signal to indicate that it has output
+ * any remaining buffers before we release it.
+ */
+ private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;
+
+ private static final int MSG_UPDATE_OUTPUT = 0;
+
+ private final Handler outputHandler;
+ private final Output output;
+ private final SubtitleDecoderFactory decoderFactory;
+ private final FormatHolder formatHolder;
+
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+ @ReplacementState private int decoderReplacementState;
+ private Format streamFormat;
+ private SubtitleDecoder decoder;
+ private SubtitleInputBuffer nextInputBuffer;
+ private SubtitleOutputBuffer subtitle;
+ private SubtitleOutputBuffer nextSubtitle;
+ private int nextSubtitleEventIndex;
+
+ /**
+ * @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 TextRenderer(Output output, Looper outputLooper) {
+ this(output, outputLooper, SubtitleDecoderFactory.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 SubtitleDecoder} instances.
+ */
+ public TextRenderer(Output output, Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
+ super(C.TRACK_TYPE_TEXT);
+ this.output = Assertions.checkNotNull(output);
+ this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
+ this.decoderFactory = decoderFactory;
+ formatHolder = new FormatHolder();
+ }
+
+ @Override
+ public int supportsFormat(Format format) {
+ return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED
+ : (MimeTypes.isText(format.sampleMimeType) ? FORMAT_UNSUPPORTED_SUBTYPE
+ : FORMAT_UNSUPPORTED_TYPE);
+ }
+
+ @Override
+ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ streamFormat = formats[0];
+ if (decoder != null) {
+ decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ decoder = decoderFactory.createDecoder(streamFormat);
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) {
+ clearOutput();
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
+ replaceDecoder();
+ } else {
+ releaseBuffers();
+ decoder.flush();
+ }
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (outputStreamEnded) {
+ return;
+ }
+
+ if (nextSubtitle == null) {
+ decoder.setPositionUs(positionUs);
+ try {
+ nextSubtitle = decoder.dequeueOutputBuffer();
+ } catch (SubtitleDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ if (getState() != STATE_STARTED) {
+ return;
+ }
+
+ boolean textRendererNeedsUpdate = false;
+ if (subtitle != null) {
+ // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
+ // advance to the next event.
+ long subtitleNextEventTimeUs = getNextEventTime();
+ while (subtitleNextEventTimeUs <= positionUs) {
+ nextSubtitleEventIndex++;
+ subtitleNextEventTimeUs = getNextEventTime();
+ textRendererNeedsUpdate = true;
+ }
+ }
+
+ if (nextSubtitle != null) {
+ if (nextSubtitle.isEndOfStream()) {
+ if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
+ if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
+ replaceDecoder();
+ } else {
+ releaseBuffers();
+ outputStreamEnded = true;
+ }
+ }
+ } else if (nextSubtitle.timeUs <= positionUs) {
+ // Advance to the next subtitle. Sync the next event index and trigger an update.
+ if (subtitle != null) {
+ subtitle.release();
+ }
+ subtitle = nextSubtitle;
+ nextSubtitle = null;
+ nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);
+ textRendererNeedsUpdate = true;
+ }
+ }
+
+ if (textRendererNeedsUpdate) {
+ // textRendererNeedsUpdate is set and we're playing. Update the renderer.
+ updateOutput(subtitle.getCues(positionUs));
+ }
+
+ if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
+ return;
+ }
+
+ try {
+ while (!inputStreamEnded) {
+ if (nextInputBuffer == null) {
+ nextInputBuffer = decoder.dequeueInputBuffer();
+ if (nextInputBuffer == null) {
+ return;
+ }
+ }
+ if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
+ nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(nextInputBuffer);
+ nextInputBuffer = null;
+ decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
+ return;
+ }
+ // Try and read the next subtitle from the source.
+ int result = readSource(formatHolder, nextInputBuffer, false);
+ if (result == C.RESULT_BUFFER_READ) {
+ if (nextInputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ } else {
+ nextInputBuffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
+ nextInputBuffer.flip();
+ }
+ decoder.queueInputBuffer(nextInputBuffer);
+ nextInputBuffer = null;
+ } else if (result == C.RESULT_NOTHING_READ) {
+ return;
+ }
+ }
+ } catch (SubtitleDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ streamFormat = null;
+ clearOutput();
+ releaseDecoder();
+ super.onDisabled();
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ // Don't block playback whilst subtitles are loading.
+ // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
+ return true;
+ }
+
+ private void releaseBuffers() {
+ nextInputBuffer = null;
+ nextSubtitleEventIndex = C.INDEX_UNSET;
+ if (subtitle != null) {
+ subtitle.release();
+ subtitle = null;
+ }
+ if (nextSubtitle != null) {
+ nextSubtitle.release();
+ nextSubtitle = null;
+ }
+ }
+
+ private void releaseDecoder() {
+ releaseBuffers();
+ decoder.release();
+ decoder = null;
+ decoderReplacementState = REPLACEMENT_STATE_NONE;
+ }
+
+ private void replaceDecoder() {
+ releaseDecoder();
+ decoder = decoderFactory.createDecoder(streamFormat);
+ }
+
+ private long getNextEventTime() {
+ return ((nextSubtitleEventIndex == C.INDEX_UNSET)
+ || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE
+ : (subtitle.getEventTime(nextSubtitleEventIndex));
+ }
+
+ private void updateOutput(List<Cue> cues) {
+ if (outputHandler != null) {
+ outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
+ } else {
+ invokeUpdateOutputInternal(cues);
+ }
+ }
+
+ private void clearOutput() {
+ updateOutput(Collections.<Cue>emptyList());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_OUTPUT:
+ invokeUpdateOutputInternal((List<Cue>) msg.obj);
+ return true;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private void invokeUpdateOutputInternal(List<Cue> cues) {
+ output.onCues(cues);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -0,0 +1,788 @@
+/*
+ * 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 com.google.android.exoplayer2.text.cea;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
+ */
+public final class Cea608Decoder extends CeaDecoder {
+
+ private static final int CC_VALID_FLAG = 0x04;
+ private static final int CC_TYPE_FLAG = 0x02;
+ private static final int CC_FIELD_FLAG = 0x01;
+
+ private static final int NTSC_CC_FIELD_1 = 0x00;
+ private static final int NTSC_CC_FIELD_2 = 0x01;
+ private static final int CC_VALID_608_ID = 0x04;
+
+ private static final int CC_MODE_UNKNOWN = 0;
+ private static final int CC_MODE_ROLL_UP = 1;
+ private static final int CC_MODE_POP_ON = 2;
+ private static final int CC_MODE_PAINT_ON = 3;
+
+ private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
+ private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
+ private static final int[] COLORS = new int[] {
+ Color.WHITE,
+ Color.GREEN,
+ Color.BLUE,
+ Color.CYAN,
+ Color.RED,
+ Color.YELLOW,
+ Color.MAGENTA,
+ };
+
+ // The default number of rows to display in roll-up captions mode.
+ private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
+
+ // An implied first byte for packets that are only 2 bytes long, consisting of marker bits
+ // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00).
+ private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC;
+
+ /**
+ * Command initiating pop-on style captioning. Subsequent data should be loaded into a
+ * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received,
+ * at which point the non-displayed memory becomes the displayed memory (and vice versa).
+ */
+ private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 2 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_2_ROWS = 0x25;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 3 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_3_ROWS = 0x26;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 4 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
+ /**
+ * Command initiating paint-on style captioning. Subsequent data should be addressed immediately
+ * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.
+ */
+ private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;
+ /**
+ * Command indicating the end of a pop-on style caption. At this point the caption loaded in
+ * non-displayed memory should be swapped with the one in displayed memory. If no
+ * {@link #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the
+ * receiver into pop-on style.
+ */
+ private static final byte CTRL_END_OF_CAPTION = 0x2F;
+
+ private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
+ private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
+ private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
+ private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
+
+ private static final byte CTRL_BACKSPACE = 0x21;
+
+ // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
+ private static final int[] BASIC_CHARACTER_SET = new int[] {
+ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & '
+ 0x28, 0x29, // ( )
+ 0xE1, // 2A: 225 'Ă¡' "Latin small letter A with acute"
+ 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . /
+ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7
+ 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ?
+ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G
+ 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O
+ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W
+ 0x58, 0x59, 0x5A, 0x5B, // X Y Z [
+ 0xE9, // 5C: 233 'Ă©' "Latin small letter E with acute"
+ 0x5D, // ]
+ 0xED, // 5E: 237 'Ă' "Latin small letter I with acute"
+ 0xF3, // 5F: 243 'Ă³' "Latin small letter O with acute"
+ 0xFA, // 60: 250 'Ăº' "Latin small letter U with acute"
+ 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g
+ 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o
+ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w
+ 0x78, 0x79, 0x7A, // x y z
+ 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
+ 0xF7, // 7C: 247 'Ă·' "Division sign"
+ 0xD1, // 7D: 209 'Ă‘' "Latin capital letter N with tilde"
+ 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
+ 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block)
+ };
+
+ // Special North American 608 CC char set.
+ private static final int[] SPECIAL_CHARACTER_SET = new int[] {
+ 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
+ 0xB0, // 31: 176 '°' "Degree Sign"
+ 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
+ 0xBF, // 33: 191 '¿' "Inverted Question Mark"
+ 0x2122, // 34: "Trade Mark Sign" (tm superscript)
+ 0xA2, // 35: 162 '¢' "Cent Sign"
+ 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling
+ 0x266A, // 37: "Eighth Note" - music note
+ 0xE0, // 38: 224 'Ă ' "Latin small letter A with grave"
+ 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space
+ 0xE8, // 3A: 232 'è' "Latin small letter E with grave"
+ 0xE2, // 3B: 226 'Ă¢' "Latin small letter A with circumflex"
+ 0xEA, // 3C: 234 'Ăª' "Latin small letter E with circumflex"
+ 0xEE, // 3D: 238 'Ă®' "Latin small letter I with circumflex"
+ 0xF4, // 3E: 244 'Ă´' "Latin small letter O with circumflex"
+ 0xFB // 3F: 251 'Ă»' "Latin small letter U with circumflex"
+ };
+
+ // Extended Spanish/Miscellaneous and French char set.
+ private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
+ // Spanish and misc.
+ 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
+ 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
+ // French.
+ 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
+ 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
+ };
+
+ //Extended Portuguese and German/Danish char set.
+ private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
+ // Portuguese.
+ 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
+ 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
+ // German/Danish.
+ 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
+ 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
+ };
+
+ private final ParsableByteArray ccData;
+ private final int packetLength;
+ private final int selectedField;
+ private final LinkedList<CueBuilder> cueBuilders;
+
+ private CueBuilder currentCueBuilder;
+ private List<Cue> cues;
+ private List<Cue> lastCues;
+
+ private int captionMode;
+ private int captionRowCount;
+
+ private boolean repeatableControlSet;
+ private byte repeatableControlCc1;
+ private byte repeatableControlCc2;
+
+ public Cea608Decoder(String mimeType, int accessibilityChannel) {
+ ccData = new ParsableByteArray();
+ cueBuilders = new LinkedList<>();
+ currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
+ packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
+ switch (accessibilityChannel) {
+ case 3:
+ case 4:
+ selectedField = 2;
+ break;
+ case 1:
+ case 2:
+ case Format.NO_VALUE:
+ default:
+ selectedField = 1;
+ }
+
+ setCaptionMode(CC_MODE_UNKNOWN);
+ resetCueBuilders();
+ }
+
+ @Override
+ public String getName() {
+ return "Cea608Decoder";
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ cues = null;
+ lastCues = null;
+ setCaptionMode(CC_MODE_UNKNOWN);
+ resetCueBuilders();
+ captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
+ repeatableControlSet = false;
+ repeatableControlCc1 = 0;
+ repeatableControlCc2 = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ protected boolean isNewSubtitleDataAvailable() {
+ return cues != lastCues;
+ }
+
+ @Override
+ protected Subtitle createSubtitle() {
+ lastCues = cues;
+ return new CeaSubtitle(cues);
+ }
+
+ @Override
+ protected void decode(SubtitleInputBuffer inputBuffer) {
+ ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
+ boolean captionDataProcessed = false;
+ boolean isRepeatableControl = false;
+ while (ccData.bytesLeft() >= packetLength) {
+ byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER
+ : (byte) ccData.readUnsignedByte();
+ byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit
+ byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit
+
+ // Only examine valid CEA-608 packets
+ // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
+ // to the CEA-608 specification. We need to determine if the data should be handled
+ // differently when that is not the case.
+ if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) {
+ continue;
+ }
+
+ // Only examine packets within the selected field
+ if ((selectedField == 1 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_1)
+ || (selectedField == 2 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_2)) {
+ continue;
+ }
+
+ // Ignore empty captions.
+ if (ccData1 == 0 && ccData2 == 0) {
+ continue;
+ }
+
+ // If we've reached this point then there is data to process; flag that work has been done.
+ captionDataProcessed = true;
+
+ // Special North American character set.
+ // ccData1 - 0|0|0|1|C|0|0|1
+ // ccData2 - 0|0|1|1|X|X|X|X
+ if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) {
+ // TODO: Make use of the channel toggle
+ currentCueBuilder.append(getSpecialChar(ccData2));
+ continue;
+ }
+
+ // Extended Western European character set.
+ // ccData1 - 0|0|0|1|C|0|1|S
+ // ccData2 - 0|0|1|X|X|X|X|X
+ if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) {
+ // TODO: Make use of the channel toggle
+ // Remove standard equivalent of the special extended char before appending new one
+ currentCueBuilder.backspace();
+ if ((ccData1 & 0x01) == 0x00) {
+ // Extended Spanish/Miscellaneous and French character set (S = 0).
+ currentCueBuilder.append(getExtendedEsFrChar(ccData2));
+ } else {
+ // Extended Portuguese and German/Danish character set (S = 1).
+ currentCueBuilder.append(getExtendedPtDeChar(ccData2));
+ }
+ continue;
+ }
+
+ // Control character.
+ // ccData1 - 0|0|0|X|X|X|X|X
+ if ((ccData1 & 0xE0) == 0x00) {
+ isRepeatableControl = handleCtrl(ccData1, ccData2);
+ continue;
+ }
+
+ // Basic North American character set.
+ currentCueBuilder.append(getChar(ccData1));
+ if ((ccData2 & 0xE0) != 0x00) {
+ currentCueBuilder.append(getChar(ccData2));
+ }
+ }
+
+ if (captionDataProcessed) {
+ if (!isRepeatableControl) {
+ repeatableControlSet = false;
+ }
+ if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+ cues = getDisplayCues();
+ }
+ }
+ }
+
+ private boolean handleCtrl(byte cc1, byte cc2) {
+ boolean isRepeatableControl = isRepeatable(cc1);
+
+ // Most control commands are sent twice in succession to ensure they are received properly.
+ // We don't want to process duplicate commands, so if we see the same repeatable command twice
+ // in a row, ignore the second one.
+ if (isRepeatableControl) {
+ if (repeatableControlSet
+ && repeatableControlCc1 == cc1
+ && repeatableControlCc2 == cc2) {
+ // This is a duplicate. Clear the repeatable control flag and return.
+ repeatableControlSet = false;
+ return true;
+ } else {
+ // This is a repeatable command, but we haven't see it yet, so set the repeabable control
+ // flag (to ensure we ignore the next one should it be a duplicate) and continue processing
+ // the command.
+ repeatableControlSet = true;
+ repeatableControlCc1 = cc1;
+ repeatableControlCc2 = cc2;
+ }
+ }
+
+ if (isMidrowCtrlCode(cc1, cc2)) {
+ handleMidrowCtrl(cc2);
+ } else if (isPreambleAddressCode(cc1, cc2)) {
+ handlePreambleAddressCode(cc1, cc2);
+ } else if (isTabCtrlCode(cc1, cc2)) {
+ currentCueBuilder.setTab(cc2 - 0x20);
+ } else if (isMiscCode(cc1, cc2)) {
+ handleMiscCode(cc2);
+ }
+
+ return isRepeatableControl;
+ }
+
+ private void handleMidrowCtrl(byte cc2) {
+ // TODO: support the extended styles (i.e. backgrounds and transparencies)
+
+ // cc2 - 0|0|1|0|ATRBT|U
+ // ATRBT is the 3-byte encoded attribute, and U is the underline toggle
+ boolean isUnderlined = (cc2 & 0x01) == 0x01;
+ currentCueBuilder.setUnderline(isUnderlined);
+
+ int attribute = (cc2 >> 1) & 0x0F;
+ if (attribute == 0x07) {
+ currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2);
+ currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1);
+ } else {
+ currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1);
+ }
+ }
+
+ private void handlePreambleAddressCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|E|ROW
+ // C is the channel toggle, E is the extended flag, and ROW is the encoded row
+ int row = ROW_INDICES[cc1 & 0x07];
+ // TODO: Make use of the channel toggle
+ // TODO: support the extended address and style
+
+ // cc2 - 0|1|N|ATTRBTE|U
+ // N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the
+ // underline toggle.
+ boolean nextRowDown = (cc2 & 0x20) != 0;
+ if (nextRowDown) {
+ row++;
+ }
+
+ if (row != currentCueBuilder.getRow()) {
+ if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+ currentCueBuilder = new CueBuilder(captionMode, captionRowCount);
+ cueBuilders.add(currentCueBuilder);
+ }
+ currentCueBuilder.setRow(row);
+ }
+
+ if ((cc2 & 0x01) == 0x01) {
+ currentCueBuilder.setPreambleStyle(new UnderlineSpan());
+ }
+
+ // cc2 - 0|1|N|0|STYLE|U
+ // cc2 - 0|1|N|1|CURSR|U
+ int attribute = cc2 >> 1 & 0x0F;
+ if (attribute <= 0x07) {
+ if (attribute == 0x07) {
+ currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC));
+ currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE));
+ } else {
+ currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute]));
+ }
+ } else {
+ currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]);
+ }
+ }
+
+ private void handleMiscCode(byte cc2) {
+ switch (cc2) {
+ case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
+ captionRowCount = 2;
+ setCaptionMode(CC_MODE_ROLL_UP);
+ return;
+ case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
+ captionRowCount = 3;
+ setCaptionMode(CC_MODE_ROLL_UP);
+ return;
+ case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
+ captionRowCount = 4;
+ setCaptionMode(CC_MODE_ROLL_UP);
+ return;
+ case CTRL_RESUME_CAPTION_LOADING:
+ setCaptionMode(CC_MODE_POP_ON);
+ return;
+ case CTRL_RESUME_DIRECT_CAPTIONING:
+ setCaptionMode(CC_MODE_PAINT_ON);
+ return;
+ }
+
+ if (captionMode == CC_MODE_UNKNOWN) {
+ return;
+ }
+
+ switch (cc2) {
+ case CTRL_ERASE_DISPLAYED_MEMORY:
+ cues = null;
+ if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+ resetCueBuilders();
+ }
+ break;
+ case CTRL_ERASE_NON_DISPLAYED_MEMORY:
+ resetCueBuilders();
+ break;
+ case CTRL_END_OF_CAPTION:
+ cues = getDisplayCues();
+ resetCueBuilders();
+ break;
+ case CTRL_CARRIAGE_RETURN:
+ // carriage returns only apply to rollup captions; don't bother if we don't have anything
+ // to add a carriage return to
+ if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+ currentCueBuilder.rollUp();
+ }
+ break;
+ case CTRL_BACKSPACE:
+ currentCueBuilder.backspace();
+ break;
+ case CTRL_DELETE_TO_END_OF_ROW:
+ // TODO: implement
+ break;
+ }
+ }
+
+ private List<Cue> getDisplayCues() {
+ List<Cue> displayCues = new ArrayList<>();
+ for (int i = 0; i < cueBuilders.size(); i++) {
+ Cue cue = cueBuilders.get(i).build();
+ if (cue != null) {
+ displayCues.add(cue);
+ }
+ }
+ return displayCues;
+ }
+
+ private void setCaptionMode(int captionMode) {
+ if (this.captionMode == captionMode) {
+ return;
+ }
+
+ int oldCaptionMode = this.captionMode;
+ this.captionMode = captionMode;
+
+ // Clear the working memory.
+ resetCueBuilders();
+ if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP
+ || captionMode == CC_MODE_UNKNOWN) {
+ // When switching from paint-on or to roll-up or unknown, we also need to clear the caption.
+ cues = null;
+ }
+ }
+
+ private void resetCueBuilders() {
+ currentCueBuilder.reset(captionMode, captionRowCount);
+ cueBuilders.clear();
+ cueBuilders.add(currentCueBuilder);
+ }
+
+ private static char getChar(byte ccData) {
+ int index = (ccData & 0x7F) - 0x20;
+ return (char) BASIC_CHARACTER_SET[index];
+ }
+
+ private static char getSpecialChar(byte ccData) {
+ int index = ccData & 0x0F;
+ return (char) SPECIAL_CHARACTER_SET[index];
+ }
+
+ private static char getExtendedEsFrChar(byte ccData) {
+ int index = ccData & 0x1F;
+ return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
+ }
+
+ private static char getExtendedPtDeChar(byte ccData) {
+ int index = ccData & 0x1F;
+ return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
+ }
+
+ private static boolean isMidrowCtrlCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|0|0|1
+ // cc2 - 0|0|1|0|X|X|X|X
+ return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20);
+ }
+
+ private static boolean isPreambleAddressCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|X|X|X
+ // cc2 - 0|1|X|X|X|X|X|X
+ return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x40);
+ }
+
+ private static boolean isTabCtrlCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|1|1|1
+ // cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1
+ return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);
+ }
+
+ private static boolean isMiscCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|1|0|0
+ // cc2 - 0|0|1|0|X|X|X|X
+ return ((cc1 & 0xF7) == 0x14) && ((cc2 & 0xF0) == 0x20);
+ }
+
+ private static boolean isRepeatable(byte cc1) {
+ // cc1 - 0|0|0|1|X|X|X|X
+ return (cc1 & 0xF0) == 0x10;
+ }
+
+ private static class CueBuilder {
+
+ private static final int POSITION_UNSET = -1;
+
+ // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
+ // positions to normalized screen position.
+ private static final int SCREEN_CHARWIDTH = 32;
+ private static final int BASE_ROW = 15;
+
+ private final List<CharacterStyle> preambleStyles;
+ private final List<CueStyle> midrowStyles;
+ private final List<SpannableString> rolledUpCaptions;
+ private final SpannableStringBuilder captionStringBuilder;
+
+ private int row;
+ private int indent;
+ private int tabOffset;
+ private int captionMode;
+ private int captionRowCount;
+ private int underlineStartPosition;
+
+ public CueBuilder(int captionMode, int captionRowCount) {
+ preambleStyles = new ArrayList<>();
+ midrowStyles = new ArrayList<>();
+ rolledUpCaptions = new LinkedList<>();
+ captionStringBuilder = new SpannableStringBuilder();
+ reset(captionMode, captionRowCount);
+ }
+
+ public void reset(int captionMode, int captionRowCount) {
+ preambleStyles.clear();
+ midrowStyles.clear();
+ rolledUpCaptions.clear();
+ captionStringBuilder.clear();
+ row = BASE_ROW;
+ indent = 0;
+ tabOffset = 0;
+ this.captionMode = captionMode;
+ this.captionRowCount = captionRowCount;
+ underlineStartPosition = POSITION_UNSET;
+ }
+
+ public boolean isEmpty() {
+ return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
+ && captionStringBuilder.length() == 0;
+ }
+
+ public void backspace() {
+ int length = captionStringBuilder.length();
+ if (length > 0) {
+ captionStringBuilder.delete(length - 1, length);
+ }
+ }
+
+ public int getRow() {
+ return row;
+ }
+
+ public void setRow(int row) {
+ this.row = row;
+ }
+
+ public void rollUp() {
+ rolledUpCaptions.add(buildSpannableString());
+ captionStringBuilder.clear();
+ preambleStyles.clear();
+ midrowStyles.clear();
+ underlineStartPosition = POSITION_UNSET;
+
+ int numRows = Math.min(captionRowCount, row);
+ while (rolledUpCaptions.size() >= numRows) {
+ rolledUpCaptions.remove(0);
+ }
+ }
+
+ public void setIndent(int indent) {
+ this.indent = indent;
+ }
+
+ public void setTab(int tabs) {
+ tabOffset = tabs;
+ }
+
+ public void setPreambleStyle(CharacterStyle style) {
+ preambleStyles.add(style);
+ }
+
+ public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) {
+ midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement));
+ }
+
+ public void setUnderline(boolean enabled) {
+ if (enabled) {
+ underlineStartPosition = captionStringBuilder.length();
+ } else if (underlineStartPosition != POSITION_UNSET) {
+ // underline spans won't overlap, so it's safe to modify the builder directly with them
+ captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ underlineStartPosition = POSITION_UNSET;
+ }
+ }
+
+ public void append(char text) {
+ captionStringBuilder.append(text);
+ }
+
+ public SpannableString buildSpannableString() {
+ int length = captionStringBuilder.length();
+
+ // preamble styles apply to the entire cue
+ for (int i = 0; i < preambleStyles.size(); i++) {
+ captionStringBuilder.setSpan(preambleStyles.get(i), 0, length,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ // midrow styles only apply to part of the cue, and after preamble styles
+ for (int i = 0; i < midrowStyles.size(); i++) {
+ CueStyle cueStyle = midrowStyles.get(i);
+ int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement)
+ ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start
+ : length;
+ captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ // special case for midrow underlines that went to the end of the cue
+ if (underlineStartPosition != POSITION_UNSET) {
+ captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return new SpannableString(captionStringBuilder);
+ }
+
+ public Cue build() {
+ SpannableStringBuilder cueString = new SpannableStringBuilder();
+ // Add any rolled up captions, separated by new lines.
+ for (int i = 0; i < rolledUpCaptions.size(); i++) {
+ cueString.append(rolledUpCaptions.get(i));
+ cueString.append('\n');
+ }
+ // Add the current line.
+ cueString.append(buildSpannableString());
+
+ if (cueString.length() == 0) {
+ // The cue is empty.
+ return null;
+ }
+
+ float position;
+ int positionAnchor;
+ // The number of empty columns before the start of the text, in the range [0-31].
+ int startPadding = indent + tabOffset;
+ // The number of empty columns after the end of the text, in the same range.
+ int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
+ int startEndPaddingDelta = startPadding - endPadding;
+ if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) {
+ // Treat approximately centered pop-on captions are middle aligned.
+ position = 0.5f;
+ positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+ } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {
+ // Treat pop-on captions with less padding at the end than the start as end aligned.
+ position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH;
+ // Adjust the position to fit within the safe area.
+ position = position * 0.8f + 0.1f;
+ positionAnchor = Cue.ANCHOR_TYPE_END;
+ } else {
+ // For all other cases assume start aligned.
+ position = (float) startPadding / SCREEN_CHARWIDTH;
+ // Adjust the position to fit within the safe area.
+ position = position * 0.8f + 0.1f;
+ positionAnchor = Cue.ANCHOR_TYPE_START;
+ }
+
+ int lineAnchor;
+ int line;
+ // Note: Row indices are in the range [1-15].
+ if (captionMode == CC_MODE_ROLL_UP || row > (BASE_ROW / 2)) {
+ lineAnchor = Cue.ANCHOR_TYPE_END;
+ line = row - BASE_ROW;
+ // Two line adjustments. The first is because line indices from the bottom of the window
+ // start from -1 rather than 0. The second is a blank row to act as the safe area.
+ line -= 2;
+ } else {
+ lineAnchor = Cue.ANCHOR_TYPE_START;
+ // Line indices from the top of the window start from 0, but we want a blank row to act as
+ // the safe area. As a result no adjustment is necessary.
+ line = row;
+ }
+
+ return new Cue(cueString, Alignment.ALIGN_NORMAL, line, Cue.LINE_TYPE_NUMBER, lineAnchor,
+ position, positionAnchor, Cue.DIMEN_UNSET);
+ }
+
+ @Override
+ public String toString() {
+ return captionStringBuilder.toString();
+ }
+
+ private static class CueStyle {
+
+ public final CharacterStyle style;
+ public final int start;
+ public final int nextStyleIncrement;
+
+ public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) {
+ this.style = style;
+ this.start = start;
+ this.nextStyleIncrement = nextStyleIncrement;
+ }
+
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.cea;
+
+import android.support.annotation.NonNull;
+import android.text.Layout.Alignment;
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A {@link Cue} for CEA-708.
+ */
+/* package */ final class Cea708Cue extends Cue implements Comparable<Cea708Cue> {
+
+ /**
+ * An unset priority.
+ */
+ public static final int PRIORITY_UNSET = -1;
+
+ /**
+ * The priority of the cue box.
+ */
+ public final int priority;
+
+ /**
+ * @param text See {@link #text}.
+ * @param textAlignment See {@link #textAlignment}.
+ * @param line See {@link #line}.
+ * @param lineType See {@link #lineType}.
+ * @param lineAnchor See {@link #lineAnchor}.
+ * @param position See {@link #position}.
+ * @param positionAnchor See {@link #positionAnchor}.
+ * @param size See {@link #size}.
+ * @param windowColorSet See {@link #windowColorSet}.
+ * @param windowColor See {@link #windowColor}.
+ * @param priority See (@link #priority}.
+ */
+ public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
+ @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
+ boolean windowColorSet, int windowColor, int priority) {
+ super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size,
+ windowColorSet, windowColor);
+ this.priority = priority;
+ }
+
+ @Override
+ public int compareTo(@NonNull Cea708Cue other) {
+ if (other.priority < priority) {
+ return -1;
+ } else if (other.priority > priority) {
+ return 1;
+ }
+ return 0;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
@@ -0,0 +1,1248 @@
+/*
+ * 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 com.google.android.exoplayer2.text.cea;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Cue.AnchorType;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708").
+ */
+public final class Cea708Decoder extends CeaDecoder {
+
+ private static final String TAG = "Cea708Decoder";
+
+ private static final int NUM_WINDOWS = 8;
+
+ private static final int DTVCC_PACKET_DATA = 0x02;
+ private static final int DTVCC_PACKET_START = 0x03;
+ private static final int CC_VALID_FLAG = 0x04;
+
+ // Base Commands
+ private static final int GROUP_C0_END = 0x1F; // Miscellaneous Control Codes
+ private static final int GROUP_G0_END = 0x7F; // ASCII Printable Characters
+ private static final int GROUP_C1_END = 0x9F; // Captioning Command Control Codes
+ private static final int GROUP_G1_END = 0xFF; // ISO 8859-1 LATIN-1 Character Set
+
+ // Extended Commands
+ private static final int GROUP_C2_END = 0x1F; // Extended Control Code Set 1
+ private static final int GROUP_G2_END = 0x7F; // Extended Miscellaneous Characters
+ private static final int GROUP_C3_END = 0x9F; // Extended Control Code Set 2
+ private static final int GROUP_G3_END = 0xFF; // Future Expansion
+
+ // Group C0 Commands
+ private static final int COMMAND_NUL = 0x00; // Nul
+ private static final int COMMAND_ETX = 0x03; // EndOfText
+ private static final int COMMAND_BS = 0x08; // Backspace
+ private static final int COMMAND_FF = 0x0C; // FormFeed (Flush)
+ private static final int COMMAND_CR = 0x0D; // CarriageReturn
+ private static final int COMMAND_HCR = 0x0E; // ClearLine
+ private static final int COMMAND_EXT1 = 0x10; // Extended Control Code Flag
+ private static final int COMMAND_EXT1_START = 0x11;
+ private static final int COMMAND_EXT1_END = 0x17;
+ private static final int COMMAND_P16_START = 0x18;
+ private static final int COMMAND_P16_END = 0x1F;
+
+ // Group C1 Commands
+ private static final int COMMAND_CW0 = 0x80; // SetCurrentWindow to 0
+ private static final int COMMAND_CW1 = 0x81; // SetCurrentWindow to 1
+ private static final int COMMAND_CW2 = 0x82; // SetCurrentWindow to 2
+ private static final int COMMAND_CW3 = 0x83; // SetCurrentWindow to 3
+ private static final int COMMAND_CW4 = 0x84; // SetCurrentWindow to 4
+ private static final int COMMAND_CW5 = 0x85; // SetCurrentWindow to 5
+ private static final int COMMAND_CW6 = 0x86; // SetCurrentWindow to 6
+ private static final int COMMAND_CW7 = 0x87; // SetCurrentWindow to 7
+ private static final int COMMAND_CLW = 0x88; // ClearWindows (+1 byte)
+ private static final int COMMAND_DSW = 0x89; // DisplayWindows (+1 byte)
+ private static final int COMMAND_HDW = 0x8A; // HideWindows (+1 byte)
+ private static final int COMMAND_TGW = 0x8B; // ToggleWindows (+1 byte)
+ private static final int COMMAND_DLW = 0x8C; // DeleteWindows (+1 byte)
+ private static final int COMMAND_DLY = 0x8D; // Delay (+1 byte)
+ private static final int COMMAND_DLC = 0x8E; // DelayCancel
+ private static final int COMMAND_RST = 0x8F; // Reset
+ private static final int COMMAND_SPA = 0x90; // SetPenAttributes (+2 bytes)
+ private static final int COMMAND_SPC = 0x91; // SetPenColor (+3 bytes)
+ private static final int COMMAND_SPL = 0x92; // SetPenLocation (+2 bytes)
+ private static final int COMMAND_SWA = 0x97; // SetWindowAttributes (+4 bytes)
+ private static final int COMMAND_DF0 = 0x98; // DefineWindow 0 (+6 bytes)
+ private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes)
+ private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes)
+ private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes)
+ private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes)
+ private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes)
+ private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes)
+ private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes)
+
+ // G0 Table Special Chars
+ private static final int CHARACTER_MN = 0x7F; // MusicNote
+
+ // G2 Table Special Chars
+ private static final int CHARACTER_TSP = 0x20;
+ private static final int CHARACTER_NBTSP = 0x21;
+ private static final int CHARACTER_ELLIPSIS = 0x25;
+ private static final int CHARACTER_BIG_CARONS = 0x2A;
+ private static final int CHARACTER_BIG_OE = 0x2C;
+ private static final int CHARACTER_SOLID_BLOCK = 0x30;
+ private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31;
+ private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32;
+ private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33;
+ private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34;
+ private static final int CHARACTER_BOLD_BULLET = 0x35;
+ private static final int CHARACTER_TM = 0x39;
+ private static final int CHARACTER_SMALL_CARONS = 0x3A;
+ private static final int CHARACTER_SMALL_OE = 0x3C;
+ private static final int CHARACTER_SM = 0x3D;
+ private static final int CHARACTER_DIAERESIS_Y = 0x3F;
+ private static final int CHARACTER_ONE_EIGHTH = 0x76;
+ private static final int CHARACTER_THREE_EIGHTHS = 0x77;
+ private static final int CHARACTER_FIVE_EIGHTHS = 0x78;
+ private static final int CHARACTER_SEVEN_EIGHTHS = 0x79;
+ private static final int CHARACTER_VERTICAL_BORDER = 0x7A;
+ private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B;
+ private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C;
+ private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D;
+ private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;
+ private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;
+
+ private final ParsableByteArray ccData;
+ private final ParsableBitArray serviceBlockPacket;
+
+ private final int selectedServiceNumber;
+ private final CueBuilder[] cueBuilders;
+
+ private CueBuilder currentCueBuilder;
+ private List<Cue> cues;
+ private List<Cue> lastCues;
+
+ private DtvCcPacket currentDtvCcPacket;
+ private int currentWindow;
+
+ public Cea708Decoder(int accessibilityChannel) {
+ ccData = new ParsableByteArray();
+ serviceBlockPacket = new ParsableBitArray();
+ selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel;
+
+ cueBuilders = new CueBuilder[NUM_WINDOWS];
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ cueBuilders[i] = new CueBuilder();
+ }
+
+ currentCueBuilder = cueBuilders[0];
+ resetCueBuilders();
+ }
+
+ @Override
+ public String getName() {
+ return "Cea708Decoder";
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ cues = null;
+ lastCues = null;
+ currentWindow = 0;
+ currentCueBuilder = cueBuilders[currentWindow];
+ resetCueBuilders();
+ currentDtvCcPacket = null;
+ }
+
+ @Override
+ protected boolean isNewSubtitleDataAvailable() {
+ return cues != lastCues;
+ }
+
+ @Override
+ protected Subtitle createSubtitle() {
+ lastCues = cues;
+ return new CeaSubtitle(cues);
+ }
+
+ @Override
+ protected void decode(SubtitleInputBuffer inputBuffer) {
+ ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
+ while (ccData.bytesLeft() >= 3) {
+ int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);
+
+ int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START);
+ boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG;
+ byte ccData1 = (byte) ccData.readUnsignedByte();
+ byte ccData2 = (byte) ccData.readUnsignedByte();
+
+ // Ignore any non-CEA-708 data
+ if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) {
+ continue;
+ }
+
+ if (!ccValid) {
+ // This byte-pair isn't valid, ignore it and continue.
+ continue;
+ }
+
+ if (ccType == DTVCC_PACKET_START) {
+ finalizeCurrentPacket();
+
+ int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
+ int packetSize = ccData1 & 0x3F; // last 6 bits
+ if (packetSize == 0) {
+ packetSize = 64;
+ }
+
+ currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize);
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
+ } else {
+ // The only remaining valid packet type is DTVCC_PACKET_DATA
+ Assertions.checkArgument(ccType == DTVCC_PACKET_DATA);
+
+ if (currentDtvCcPacket == null) {
+ Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START");
+ continue;
+ }
+
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1;
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
+ }
+
+ if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) {
+ finalizeCurrentPacket();
+ }
+ }
+ }
+
+ private void finalizeCurrentPacket() {
+ if (currentDtvCcPacket == null) {
+ // No packet to finalize;
+ return;
+ }
+
+ processCurrentPacket();
+ currentDtvCcPacket = null;
+ }
+
+ private void processCurrentPacket() {
+ if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {
+ Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1)
+ + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number "
+ + currentDtvCcPacket.sequenceNumber + "); ignoring packet");
+ return;
+ }
+
+ serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);
+
+ int serviceNumber = serviceBlockPacket.readBits(3);
+ int blockSize = serviceBlockPacket.readBits(5);
+ if (serviceNumber == 7) {
+ // extended service numbers
+ serviceBlockPacket.skipBits(2);
+ serviceNumber += serviceBlockPacket.readBits(6);
+ }
+
+ // Ignore packets in which blockSize is 0
+ if (blockSize == 0) {
+ if (serviceNumber != 0) {
+ Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
+ }
+ return;
+ }
+
+ if (serviceNumber != selectedServiceNumber) {
+ return;
+ }
+
+ // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after
+ // processing the service block any text has been added to the buffer. See CEA-708-B Section
+ // 8.10.4 for more details.
+ boolean cuesNeedUpdate = false;
+
+ while (serviceBlockPacket.bitsLeft() > 0) {
+ int command = serviceBlockPacket.readBits(8);
+ if (command != COMMAND_EXT1) {
+ if (command <= GROUP_C0_END) {
+ handleC0Command(command);
+ // If the C0 command was an ETX command, the cues are updated in handleC0Command.
+ } else if (command <= GROUP_G0_END) {
+ handleG0Character(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_C1_END) {
+ handleC1Command(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_G1_END) {
+ handleG1Character(command);
+ cuesNeedUpdate = true;
+ } else {
+ Log.w(TAG, "Invalid base command: " + command);
+ }
+ } else {
+ // Read the extended command
+ command = serviceBlockPacket.readBits(8);
+ if (command <= GROUP_C2_END) {
+ handleC2Command(command);
+ } else if (command <= GROUP_G2_END) {
+ handleG2Character(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_C3_END) {
+ handleC3Command(command);
+ } else if (command <= GROUP_G3_END) {
+ handleG3Character(command);
+ cuesNeedUpdate = true;
+ } else {
+ Log.w(TAG, "Invalid extended command: " + command);
+ }
+ }
+ }
+
+ if (cuesNeedUpdate) {
+ cues = getDisplayCues();
+ }
+ }
+
+ private void handleC0Command(int command) {
+ switch (command) {
+ case COMMAND_NUL:
+ // Do nothing.
+ break;
+ case COMMAND_ETX:
+ cues = getDisplayCues();
+ break;
+ case COMMAND_BS:
+ currentCueBuilder.backspace();
+ break;
+ case COMMAND_FF:
+ resetCueBuilders();
+ break;
+ case COMMAND_CR:
+ currentCueBuilder.append('\n');
+ break;
+ case COMMAND_HCR:
+ // TODO: Add support for this command.
+ break;
+ default:
+ if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) {
+ Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command);
+ serviceBlockPacket.skipBits(8);
+ } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) {
+ Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command);
+ serviceBlockPacket.skipBits(16);
+ } else {
+ Log.w(TAG, "Invalid C0 command: " + command);
+ }
+ }
+ }
+
+ private void handleC1Command(int command) {
+ int window;
+ switch (command) {
+ case COMMAND_CW0:
+ case COMMAND_CW1:
+ case COMMAND_CW2:
+ case COMMAND_CW3:
+ case COMMAND_CW4:
+ case COMMAND_CW5:
+ case COMMAND_CW6:
+ case COMMAND_CW7:
+ window = (command - COMMAND_CW0);
+ if (currentWindow != window) {
+ currentWindow = window;
+ currentCueBuilder = cueBuilders[window];
+ }
+ break;
+ case COMMAND_CLW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].clear();
+ }
+ }
+ break;
+ case COMMAND_DSW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].setVisibility(true);
+ }
+ }
+ break;
+ case COMMAND_HDW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].setVisibility(false);
+ }
+ }
+ break;
+ case COMMAND_TGW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i];
+ cueBuilder.setVisibility(!cueBuilder.isVisible());
+ }
+ }
+ break;
+ case COMMAND_DLW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].reset();
+ }
+ }
+ break;
+ case COMMAND_DLY:
+ // TODO: Add support for delay commands.
+ serviceBlockPacket.skipBits(8);
+ break;
+ case COMMAND_DLC:
+ // TODO: Add support for delay commands.
+ break;
+ case COMMAND_RST:
+ resetCueBuilders();
+ break;
+ case COMMAND_SPA:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(16);
+ } else {
+ handleSetPenAttributes();
+ }
+ break;
+ case COMMAND_SPC:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(24);
+ } else {
+ handleSetPenColor();
+ }
+ break;
+ case COMMAND_SPL:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(16);
+ } else {
+ handleSetPenLocation();
+ }
+ break;
+ case COMMAND_SWA:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(32);
+ } else {
+ handleSetWindowAttributes();
+ }
+ break;
+ case COMMAND_DF0:
+ case COMMAND_DF1:
+ case COMMAND_DF2:
+ case COMMAND_DF3:
+ case COMMAND_DS4:
+ case COMMAND_DF5:
+ case COMMAND_DF6:
+ case COMMAND_DF7:
+ window = (command - COMMAND_DF0);
+ handleDefineWindow(window);
+ // We also set the current window to the newly defined window.
+ if (currentWindow != window) {
+ currentWindow = window;
+ currentCueBuilder = cueBuilders[window];
+ }
+ break;
+ default:
+ Log.w(TAG, "Invalid C1 command: " + command);
+ }
+ }
+
+ private void handleC2Command(int command) {
+ // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
+ if (command <= 0x07) {
+ // Do nothing.
+ } else if (command <= 0x0F) {
+ serviceBlockPacket.skipBits(8);
+ } else if (command <= 0x17) {
+ serviceBlockPacket.skipBits(16);
+ } else if (command <= 0x1F) {
+ serviceBlockPacket.skipBits(24);
+ }
+ }
+
+ private void handleC3Command(int command) {
+ // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
+ if (command <= 0x87) {
+ serviceBlockPacket.skipBits(32);
+ } else if (command <= 0x8F) {
+ serviceBlockPacket.skipBits(40);
+ } else if (command <= 0x9F) {
+ // 90-9F are variable length codes; the first byte defines the header with the first
+ // 2 bits specifying the type and the last 6 bits specifying the remaining length of the
+ // command in bytes
+ serviceBlockPacket.skipBits(2);
+ int length = serviceBlockPacket.readBits(6);
+ serviceBlockPacket.skipBits(8 * length);
+ }
+ }
+
+ private void handleG0Character(int characterCode) {
+ if (characterCode == CHARACTER_MN) {
+ currentCueBuilder.append('\u266B');
+ } else {
+ currentCueBuilder.append((char) (characterCode & 0xFF));
+ }
+ }
+
+ private void handleG1Character(int characterCode) {
+ currentCueBuilder.append((char) (characterCode & 0xFF));
+ }
+
+ private void handleG2Character(int characterCode) {
+ switch (characterCode) {
+ case CHARACTER_TSP:
+ currentCueBuilder.append('\u0020');
+ break;
+ case CHARACTER_NBTSP:
+ currentCueBuilder.append('\u00A0');
+ break;
+ case CHARACTER_ELLIPSIS:
+ currentCueBuilder.append('\u2026');
+ break;
+ case CHARACTER_BIG_CARONS:
+ currentCueBuilder.append('\u0160');
+ break;
+ case CHARACTER_BIG_OE:
+ currentCueBuilder.append('\u0152');
+ break;
+ case CHARACTER_SOLID_BLOCK:
+ currentCueBuilder.append('\u2588');
+ break;
+ case CHARACTER_OPEN_SINGLE_QUOTE:
+ currentCueBuilder.append('\u2018');
+ break;
+ case CHARACTER_CLOSE_SINGLE_QUOTE:
+ currentCueBuilder.append('\u2019');
+ break;
+ case CHARACTER_OPEN_DOUBLE_QUOTE:
+ currentCueBuilder.append('\u201C');
+ break;
+ case CHARACTER_CLOSE_DOUBLE_QUOTE:
+ currentCueBuilder.append('\u201D');
+ break;
+ case CHARACTER_BOLD_BULLET:
+ currentCueBuilder.append('\u2022');
+ break;
+ case CHARACTER_TM:
+ currentCueBuilder.append('\u2122');
+ break;
+ case CHARACTER_SMALL_CARONS:
+ currentCueBuilder.append('\u0161');
+ break;
+ case CHARACTER_SMALL_OE:
+ currentCueBuilder.append('\u0153');
+ break;
+ case CHARACTER_SM:
+ currentCueBuilder.append('\u2120');
+ break;
+ case CHARACTER_DIAERESIS_Y:
+ currentCueBuilder.append('\u0178');
+ break;
+ case CHARACTER_ONE_EIGHTH:
+ currentCueBuilder.append('\u215B');
+ break;
+ case CHARACTER_THREE_EIGHTHS:
+ currentCueBuilder.append('\u215C');
+ break;
+ case CHARACTER_FIVE_EIGHTHS:
+ currentCueBuilder.append('\u215D');
+ break;
+ case CHARACTER_SEVEN_EIGHTHS:
+ currentCueBuilder.append('\u215E');
+ break;
+ case CHARACTER_VERTICAL_BORDER:
+ currentCueBuilder.append('\u2502');
+ break;
+ case CHARACTER_UPPER_RIGHT_BORDER:
+ currentCueBuilder.append('\u2510');
+ break;
+ case CHARACTER_LOWER_LEFT_BORDER:
+ currentCueBuilder.append('\u2514');
+ break;
+ case CHARACTER_HORIZONTAL_BORDER:
+ currentCueBuilder.append('\u2500');
+ break;
+ case CHARACTER_LOWER_RIGHT_BORDER:
+ currentCueBuilder.append('\u2518');
+ break;
+ case CHARACTER_UPPER_LEFT_BORDER:
+ currentCueBuilder.append('\u250C');
+ break;
+ default:
+ Log.w(TAG, "Invalid G2 character: " + characterCode);
+ // The CEA-708 specification doesn't specify what to do in the case of an unexpected
+ // value in the G2 character range, so we ignore it.
+ }
+ }
+
+ private void handleG3Character(int characterCode) {
+ if (characterCode == 0xA0) {
+ currentCueBuilder.append('\u33C4');
+ } else {
+ Log.w(TAG, "Invalid G3 character: " + characterCode);
+ // Substitute any unsupported G3 character with an underscore as per CEA-708 specification.
+ currentCueBuilder.append('_');
+ }
+ }
+
+ private void handleSetPenAttributes() {
+ // the SetPenAttributes command contains 2 bytes of data
+ // first byte
+ int textTag = serviceBlockPacket.readBits(4);
+ int offset = serviceBlockPacket.readBits(2);
+ int penSize = serviceBlockPacket.readBits(2);
+ // second byte
+ boolean italicsToggle = serviceBlockPacket.readBit();
+ boolean underlineToggle = serviceBlockPacket.readBit();
+ int edgeType = serviceBlockPacket.readBits(3);
+ int fontStyle = serviceBlockPacket.readBits(3);
+
+ currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle,
+ edgeType, fontStyle);
+ }
+
+ private void handleSetPenColor() {
+ // the SetPenColor command contains 3 bytes of data
+ // first byte
+ int foregroundO = serviceBlockPacket.readBits(2);
+ int foregroundR = serviceBlockPacket.readBits(2);
+ int foregroundG = serviceBlockPacket.readBits(2);
+ int foregroundB = serviceBlockPacket.readBits(2);
+ int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB,
+ foregroundO);
+ // second byte
+ int backgroundO = serviceBlockPacket.readBits(2);
+ int backgroundR = serviceBlockPacket.readBits(2);
+ int backgroundG = serviceBlockPacket.readBits(2);
+ int backgroundB = serviceBlockPacket.readBits(2);
+ int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB,
+ backgroundO);
+ // third byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int edgeR = serviceBlockPacket.readBits(2);
+ int edgeG = serviceBlockPacket.readBits(2);
+ int edgeB = serviceBlockPacket.readBits(2);
+ int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB);
+
+ currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor);
+ }
+
+ private void handleSetPenLocation() {
+ // the SetPenLocation command contains 2 bytes of data
+ // first byte
+ serviceBlockPacket.skipBits(4);
+ int row = serviceBlockPacket.readBits(4);
+ // second byte
+ serviceBlockPacket.skipBits(2);
+ int column = serviceBlockPacket.readBits(6);
+
+ currentCueBuilder.setPenLocation(row, column);
+ }
+
+ private void handleSetWindowAttributes() {
+ // the SetWindowAttributes command contains 4 bytes of data
+ // first byte
+ int fillO = serviceBlockPacket.readBits(2);
+ int fillR = serviceBlockPacket.readBits(2);
+ int fillG = serviceBlockPacket.readBits(2);
+ int fillB = serviceBlockPacket.readBits(2);
+ int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO);
+ // second byte
+ int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType
+ int borderR = serviceBlockPacket.readBits(2);
+ int borderG = serviceBlockPacket.readBits(2);
+ int borderB = serviceBlockPacket.readBits(2);
+ int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB);
+ // third byte
+ if (serviceBlockPacket.readBit()) {
+ borderType |= 0x04; // set the top bit of the 3-bit borderType
+ }
+ boolean wordWrapToggle = serviceBlockPacket.readBit();
+ int printDirection = serviceBlockPacket.readBits(2);
+ int scrollDirection = serviceBlockPacket.readBits(2);
+ int justification = serviceBlockPacket.readBits(2);
+ // fourth byte
+ // Note that we don't intend to support display effects
+ serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2)
+
+ currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType,
+ printDirection, scrollDirection, justification);
+ }
+
+ private void handleDefineWindow(int window) {
+ CueBuilder cueBuilder = cueBuilders[window];
+
+ // the DefineWindow command contains 6 bytes of data
+ // first byte
+ serviceBlockPacket.skipBits(2); // null padding
+ boolean visible = serviceBlockPacket.readBit();
+ boolean rowLock = serviceBlockPacket.readBit();
+ boolean columnLock = serviceBlockPacket.readBit();
+ int priority = serviceBlockPacket.readBits(3);
+ // second byte
+ boolean relativePositioning = serviceBlockPacket.readBit();
+ int verticalAnchor = serviceBlockPacket.readBits(7);
+ // third byte
+ int horizontalAnchor = serviceBlockPacket.readBits(8);
+ // fourth byte
+ int anchorId = serviceBlockPacket.readBits(4);
+ int rowCount = serviceBlockPacket.readBits(4);
+ // fifth byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int columnCount = serviceBlockPacket.readBits(6);
+ // sixth byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int windowStyle = serviceBlockPacket.readBits(3);
+ int penStyle = serviceBlockPacket.readBits(3);
+
+ cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning,
+ verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle);
+ }
+
+ private List<Cue> getDisplayCues() {
+ List<Cea708Cue> displayCues = new ArrayList<>();
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) {
+ displayCues.add(cueBuilders[i].build());
+ }
+ }
+ Collections.sort(displayCues);
+ return Collections.<Cue>unmodifiableList(displayCues);
+ }
+
+ private void resetCueBuilders() {
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ cueBuilders[i].reset();
+ }
+ }
+
+ private static final class DtvCcPacket {
+
+ public final int sequenceNumber;
+ public final int packetSize;
+ public final byte[] packetData;
+
+ int currentIndex;
+
+ public DtvCcPacket(int sequenceNumber, int packetSize) {
+ this.sequenceNumber = sequenceNumber;
+ this.packetSize = packetSize;
+ packetData = new byte[2 * packetSize - 1];
+ currentIndex = 0;
+ }
+
+ }
+
+ // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder
+ // which could be refactored into a separate class.
+ private static final class CueBuilder {
+
+ private static final int RELATIVE_CUE_SIZE = 99;
+ private static final int VERTICAL_SIZE = 74;
+ private static final int HORIZONTAL_SIZE = 209;
+
+ private static final int DEFAULT_PRIORITY = 4;
+
+ private static final int MAXIMUM_ROW_COUNT = 15;
+
+ private static final int JUSTIFICATION_LEFT = 0;
+ private static final int JUSTIFICATION_RIGHT = 1;
+ private static final int JUSTIFICATION_CENTER = 2;
+ private static final int JUSTIFICATION_FULL = 3;
+
+ private static final int DIRECTION_LEFT_TO_RIGHT = 0;
+ private static final int DIRECTION_RIGHT_TO_LEFT = 1;
+ private static final int DIRECTION_TOP_TO_BOTTOM = 2;
+ private static final int DIRECTION_BOTTOM_TO_TOP = 3;
+
+ // TODO: Add other border/edge types when utilized.
+ private static final int BORDER_AND_EDGE_TYPE_NONE = 0;
+ private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3;
+
+ public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0);
+ public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0);
+ public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3);
+
+ // TODO: Add other sizes when utilized.
+ private static final int PEN_SIZE_STANDARD = 1;
+
+ // TODO: Add other pen font styles when utilized.
+ private static final int PEN_FONT_STYLE_DEFAULT = 0;
+ private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1;
+ private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2;
+ private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3;
+ private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4;
+
+ // TODO: Add other pen offsets when utilized.
+ private static final int PEN_OFFSET_NORMAL = 1;
+
+ // The window style properties are specified in the CEA-708 specification.
+ private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{
+ JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
+ JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
+ JUSTIFICATION_LEFT
+ };
+ private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{
+ DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
+ DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
+ DIRECTION_TOP_TO_BOTTOM
+ };
+ private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{
+ DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
+ DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
+ DIRECTION_RIGHT_TO_LEFT
+ };
+ private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{
+ false, false, false, true, true, true, false
+ };
+ private static final int[] WINDOW_STYLE_FILL = new int[]{
+ COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
+ COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK
+ };
+
+ // The pen style properties are specified in the CEA-708 specification.
+ private static final int[] PEN_STYLE_FONT_STYLE = new int[]{
+ PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,
+ PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
+ PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
+ PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
+ PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS
+ };
+ private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{
+ BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,
+ BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
+ BORDER_AND_EDGE_TYPE_UNIFORM
+ };
+ private static final int[] PEN_STYLE_BACKGROUND = new int[]{
+ COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
+ COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
+
+ private final List<SpannableString> rolledUpCaptions;
+ private final SpannableStringBuilder captionStringBuilder;
+
+ // Window/Cue properties
+ private boolean defined;
+ private boolean visible;
+ private int priority;
+ private boolean relativePositioning;
+ private int verticalAnchor;
+ private int horizontalAnchor;
+ private int anchorId;
+ private int rowCount;
+ private boolean rowLock;
+ private int justification;
+ private int windowStyleId;
+ private int penStyleId;
+ private int windowFillColor;
+
+ // Pen/Text properties
+ private int italicsStartPosition;
+ private int underlineStartPosition;
+ private int foregroundColorStartPosition;
+ private int foregroundColor;
+ private int backgroundColorStartPosition;
+ private int backgroundColor;
+ private int row;
+
+ public CueBuilder() {
+ rolledUpCaptions = new LinkedList<>();
+ captionStringBuilder = new SpannableStringBuilder();
+ reset();
+ }
+
+ public boolean isEmpty() {
+ return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0);
+ }
+
+ public void reset() {
+ clear();
+
+ defined = false;
+ visible = false;
+ priority = DEFAULT_PRIORITY;
+ relativePositioning = false;
+ verticalAnchor = 0;
+ horizontalAnchor = 0;
+ anchorId = 0;
+ rowCount = MAXIMUM_ROW_COUNT;
+ rowLock = true;
+ justification = JUSTIFICATION_LEFT;
+ windowStyleId = 0;
+ penStyleId = 0;
+ windowFillColor = COLOR_SOLID_BLACK;
+
+ foregroundColor = COLOR_SOLID_WHITE;
+ backgroundColor = COLOR_SOLID_BLACK;
+ }
+
+ public void clear() {
+ rolledUpCaptions.clear();
+ captionStringBuilder.clear();
+ italicsStartPosition = C.POSITION_UNSET;
+ underlineStartPosition = C.POSITION_UNSET;
+ foregroundColorStartPosition = C.POSITION_UNSET;
+ backgroundColorStartPosition = C.POSITION_UNSET;
+ row = 0;
+ }
+
+ public boolean isDefined() {
+ return defined;
+ }
+
+ public void setVisibility(boolean visible) {
+ this.visible = visible;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority,
+ boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount,
+ int columnCount, int anchorId, int windowStyleId, int penStyleId) {
+ this.defined = true;
+ this.visible = visible;
+ this.rowLock = rowLock;
+ this.priority = priority;
+ this.relativePositioning = relativePositioning;
+ this.verticalAnchor = verticalAnchor;
+ this.horizontalAnchor = horizontalAnchor;
+ this.anchorId = anchorId;
+
+ // Decoders must add one to rowCount to get the desired number of rows.
+ if (this.rowCount != rowCount + 1) {
+ this.rowCount = rowCount + 1;
+
+ // Trim any rolled up captions that are no longer valid, if applicable.
+ while ((rowLock && (rolledUpCaptions.size() >= this.rowCount))
+ || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
+ rolledUpCaptions.remove(0);
+ }
+ }
+
+ // TODO: Add support for column lock and count.
+
+ if (windowStyleId != 0 && this.windowStyleId != windowStyleId) {
+ this.windowStyleId = windowStyleId;
+ // windowStyleId is 1-based.
+ int windowStyleIdIndex = windowStyleId - 1;
+ // Note that Border type and border color are the same for all window styles.
+ setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT,
+ WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE,
+ WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex],
+ WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex],
+ WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]);
+ }
+
+ if (penStyleId != 0 && this.penStyleId != penStyleId) {
+ this.penStyleId = penStyleId;
+ // penStyleId is 1-based.
+ int penStyleIdIndex = penStyleId - 1;
+ // Note that pen size, offset, italics, underline, foreground color, and foreground
+ // opacity are the same for all pen styles.
+ setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false,
+ PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]);
+ setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK);
+ }
+ }
+
+
+ public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle,
+ int borderType, int printDirection, int scrollDirection, int justification) {
+ this.windowFillColor = fillColor;
+ // TODO: Add support for border color and types.
+ // TODO: Add support for word wrap.
+ // TODO: Add support for other scroll directions.
+ // TODO: Add support for other print directions.
+ this.justification = justification;
+
+ }
+
+ public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle,
+ boolean underlineToggle, int edgeType, int fontStyle) {
+ // TODO: Add support for text tags.
+ // TODO: Add support for other offsets.
+ // TODO: Add support for other pen sizes.
+
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ if (!italicsToggle) {
+ captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ italicsStartPosition = C.POSITION_UNSET;
+ }
+ } else if (italicsToggle) {
+ italicsStartPosition = captionStringBuilder.length();
+ }
+
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ if (!underlineToggle) {
+ captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ underlineStartPosition = C.POSITION_UNSET;
+ }
+ } else if (underlineToggle) {
+ underlineStartPosition = captionStringBuilder.length();
+ }
+
+ // TODO: Add support for edge types.
+ // TODO: Add support for other font styles.
+ }
+
+ public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) {
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ if (this.foregroundColor != foregroundColor) {
+ captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor),
+ foregroundColorStartPosition, captionStringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (foregroundColor != COLOR_SOLID_WHITE) {
+ foregroundColorStartPosition = captionStringBuilder.length();
+ this.foregroundColor = foregroundColor;
+ }
+
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ if (this.backgroundColor != backgroundColor) {
+ captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor),
+ backgroundColorStartPosition, captionStringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (backgroundColor != COLOR_SOLID_BLACK) {
+ backgroundColorStartPosition = captionStringBuilder.length();
+ this.backgroundColor = backgroundColor;
+ }
+
+ // TODO: Add support for edge color.
+ }
+
+ public void setPenLocation(int row, int column) {
+ // TODO: Support moving the pen location with a window properly.
+
+ // Until we support proper pen locations, if we encounter a row that's different from the
+ // previous one, we should append a new line. Otherwise, we'll see strings that should be
+ // on new lines concatenated with the previous, resulting in 2 words being combined, as
+ // well as potentially drawing beyond the width of the window/screen.
+ if (this.row != row) {
+ append('\n');
+ }
+ this.row = row;
+ }
+
+ public void backspace() {
+ int length = captionStringBuilder.length();
+ if (length > 0) {
+ captionStringBuilder.delete(length - 1, length);
+ }
+ }
+
+ public void append(char text) {
+ if (text == '\n') {
+ rolledUpCaptions.add(buildSpannableString());
+ captionStringBuilder.clear();
+
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ italicsStartPosition = 0;
+ }
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ underlineStartPosition = 0;
+ }
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ foregroundColorStartPosition = 0;
+ }
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ backgroundColorStartPosition = 0;
+ }
+
+ while ((rowLock && (rolledUpCaptions.size() >= rowCount))
+ || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
+ rolledUpCaptions.remove(0);
+ }
+ } else {
+ captionStringBuilder.append(text);
+ }
+ }
+
+ public SpannableString buildSpannableString() {
+ SpannableStringBuilder spannableStringBuilder =
+ new SpannableStringBuilder(captionStringBuilder);
+ int length = spannableStringBuilder.length();
+
+ if (length > 0) {
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
+ length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor),
+ foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor),
+ backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ return new SpannableString(spannableStringBuilder);
+ }
+
+ public Cea708Cue build() {
+ if (isEmpty()) {
+ // The cue is empty.
+ return null;
+ }
+
+ SpannableStringBuilder cueString = new SpannableStringBuilder();
+
+ // Add any rolled up captions, separated by new lines.
+ for (int i = 0; i < rolledUpCaptions.size(); i++) {
+ cueString.append(rolledUpCaptions.get(i));
+ cueString.append('\n');
+ }
+ // Add the current line.
+ cueString.append(buildSpannableString());
+
+ // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal
+ // alignment).
+ Alignment alignment;
+ switch (justification) {
+ case JUSTIFICATION_FULL:
+ // TODO: Add support for full justification.
+ case JUSTIFICATION_LEFT:
+ alignment = Alignment.ALIGN_NORMAL;
+ break;
+ case JUSTIFICATION_RIGHT:
+ alignment = Alignment.ALIGN_OPPOSITE;
+ break;
+ case JUSTIFICATION_CENTER:
+ alignment = Alignment.ALIGN_CENTER;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected justification value: " + justification);
+ }
+
+ float position;
+ float line;
+ if (relativePositioning) {
+ position = (float) horizontalAnchor / RELATIVE_CUE_SIZE;
+ line = (float) verticalAnchor / RELATIVE_CUE_SIZE;
+ } else {
+ position = (float) horizontalAnchor / HORIZONTAL_SIZE;
+ line = (float) verticalAnchor / VERTICAL_SIZE;
+ }
+ // Apply screen-edge padding to the line and position.
+ position = (position * 0.9f) + 0.05f;
+ line = (line * 0.9f) + 0.05f;
+
+ // anchorId specifies where the anchor should be placed on the caption cue/window. The 9
+ // possible configurations are as follows:
+ // 0-----1-----2
+ // | |
+ // 3 4 5
+ // | |
+ // 6-----7-----8
+ @AnchorType int verticalAnchorType;
+ if (anchorId % 3 == 0) {
+ verticalAnchorType = Cue.ANCHOR_TYPE_START;
+ } else if (anchorId % 3 == 1) {
+ verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
+ } else {
+ verticalAnchorType = Cue.ANCHOR_TYPE_END;
+ }
+ // TODO: Add support for right-to-left languages (i.e. where start is on the right).
+ @AnchorType int horizontalAnchorType;
+ if (anchorId / 3 == 0) {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_START;
+ } else if (anchorId / 3 == 1) {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
+ } else {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_END;
+ }
+
+ boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK);
+
+ return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType,
+ position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor,
+ priority);
+ }
+
+ public static int getArgbColorFromCeaColor(int red, int green, int blue) {
+ return getArgbColorFromCeaColor(red, green, blue, 0);
+ }
+
+ public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) {
+ Assertions.checkIndex(red, 0, 4);
+ Assertions.checkIndex(green, 0, 4);
+ Assertions.checkIndex(blue, 0, 4);
+ Assertions.checkIndex(opacity, 0, 4);
+
+ int alpha;
+ switch (opacity) {
+ case 0:
+ case 1:
+ // Note the value of '1' is actually FLASH, but we don't support that.
+ alpha = 255;
+ break;
+ case 2:
+ alpha = 127;
+ break;
+ case 3:
+ alpha = 0;
+ break;
+ default:
+ alpha = 255;
+ }
+
+ // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations.
+
+ // Return values based on the Minimum Color List
+ return Color.argb(alpha,
+ (red > 1 ? 255 : 0),
+ (green > 1 ? 255 : 0),
+ (blue > 1 ? 255 : 0));
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
@@ -0,0 +1,174 @@
+/*
+ * 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 com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.LinkedList;
+import java.util.TreeSet;
+
+/**
+ * Base class for subtitle parsers for CEA captions.
+ */
+/* package */ abstract class CeaDecoder implements SubtitleDecoder {
+
+ private static final int NUM_INPUT_BUFFERS = 10;
+ private static final int NUM_OUTPUT_BUFFERS = 2;
+
+ private final LinkedList<SubtitleInputBuffer> availableInputBuffers;
+ private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
+ private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
+
+ private SubtitleInputBuffer dequeuedInputBuffer;
+ private long playbackPositionUs;
+
+ public CeaDecoder() {
+ availableInputBuffers = new LinkedList<>();
+ for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
+ availableInputBuffers.add(new SubtitleInputBuffer());
+ }
+ availableOutputBuffers = new LinkedList<>();
+ for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
+ availableOutputBuffers.add(new CeaOutputBuffer(this));
+ }
+ queuedInputBuffers = new TreeSet<>();
+ }
+
+ @Override
+ public abstract String getName();
+
+ @Override
+ public void setPositionUs(long positionUs) {
+ playbackPositionUs = positionUs;
+ }
+
+ @Override
+ public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {
+ Assertions.checkState(dequeuedInputBuffer == null);
+ if (availableInputBuffers.isEmpty()) {
+ return null;
+ }
+ dequeuedInputBuffer = availableInputBuffers.pollFirst();
+ return dequeuedInputBuffer;
+ }
+
+ @Override
+ public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
+ Assertions.checkArgument(inputBuffer != null);
+ Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
+ if (inputBuffer.isDecodeOnly()) {
+ // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow
+ // for decoding to begin mid-stream.
+ releaseInputBuffer(inputBuffer);
+ } else {
+ queuedInputBuffers.add(inputBuffer);
+ }
+ dequeuedInputBuffer = null;
+ }
+
+ @Override
+ public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
+ if (availableOutputBuffers.isEmpty()) {
+ return null;
+ }
+
+ // iterate through all available input buffers whose timestamps are less than or equal
+ // to the current playback position; processing input buffers for future content should
+ // be deferred until they would be applicable
+ while (!queuedInputBuffers.isEmpty()
+ && queuedInputBuffers.first().timeUs <= playbackPositionUs) {
+ SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst();
+
+ // If the input buffer indicates we've reached the end of the stream, we can
+ // return immediately with an output buffer propagating that
+ if (inputBuffer.isEndOfStream()) {
+ SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
+ outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ releaseInputBuffer(inputBuffer);
+ return outputBuffer;
+ }
+
+ decode(inputBuffer);
+
+ // check if we have any caption updates to report
+ if (isNewSubtitleDataAvailable()) {
+ // Even if the subtitle is decode-only; we need to generate it to consume the data so it
+ // isn't accidentally prepended to the next subtitle
+ Subtitle subtitle = createSubtitle();
+ if (!inputBuffer.isDecodeOnly()) {
+ SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
+ outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE);
+ releaseInputBuffer(inputBuffer);
+ return outputBuffer;
+ }
+ }
+
+ releaseInputBuffer(inputBuffer);
+ }
+
+ return null;
+ }
+
+ private void releaseInputBuffer(SubtitleInputBuffer inputBuffer) {
+ inputBuffer.clear();
+ availableInputBuffers.add(inputBuffer);
+ }
+
+ protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {
+ outputBuffer.clear();
+ availableOutputBuffers.add(outputBuffer);
+ }
+
+ @Override
+ public void flush() {
+ playbackPositionUs = 0;
+ while (!queuedInputBuffers.isEmpty()) {
+ releaseInputBuffer(queuedInputBuffers.pollFirst());
+ }
+ if (dequeuedInputBuffer != null) {
+ releaseInputBuffer(dequeuedInputBuffer);
+ dequeuedInputBuffer = null;
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ /**
+ * Returns whether there is data available to create a new {@link Subtitle}.
+ */
+ protected abstract boolean isNewSubtitleDataAvailable();
+
+ /**
+ * Creates a {@link Subtitle} from the available data.
+ */
+ protected abstract Subtitle createSubtitle();
+
+ /**
+ * Filters and processes the raw data, providing {@link Subtitle}s via {@link #createSubtitle()}
+ * when sufficient data has been processed.
+ */
+ protected abstract void decode(SubtitleInputBuffer inputBuffer);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
+
+/**
+ * A {@link SubtitleOutputBuffer} for {@link CeaDecoder}s.
+ */
+public final class CeaOutputBuffer extends SubtitleOutputBuffer {
+
+ private final CeaDecoder owner;
+
+ /**
+ * @param owner The decoder that owns this buffer.
+ */
+ public CeaOutputBuffer(CeaDecoder owner) {
+ super();
+ this.owner = owner;
+ }
+
+ @Override
+ public final void release() {
+ owner.releaseOutputBuffer(this);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a CEA subtitle.
+ */
+/* package */ final class CeaSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ /**
+ * @param cues The subtitle cues.
+ */
+ public CeaSubtitle(List<Cue> cues) {
+ this.cues = cues;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.<Cue>emptyList();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/cea/CeaUtil.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 com.google.android.exoplayer2.text.cea;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Utility methods for handling CEA-608/708 messages.
+ */
+public final class CeaUtil {
+
+ private static final String TAG = "CeaUtil";
+
+ private static final int PAYLOAD_TYPE_CC = 4;
+ private static final int COUNTRY_CODE = 0xB5;
+ private static final int PROVIDER_CODE = 0x31;
+ private static final int USER_ID = 0x47413934; // "GA94"
+ private static final int USER_DATA_TYPE_CODE = 0x3;
+
+ /**
+ * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages
+ * as samples to all of the provided outputs.
+ *
+ * @param presentationTimeUs The presentation time in microseconds for any samples.
+ * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type.
+ * @param outputs The outputs to which any samples should be written.
+ */
+ public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,
+ TrackOutput[] outputs) {
+ while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
+ int payloadType = readNon255TerminatedValue(seiBuffer);
+ int payloadSize = readNon255TerminatedValue(seiBuffer);
+ // Process the payload.
+ if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) {
+ // This might occur if we're trying to read an encrypted SEI NAL unit.
+ Log.w(TAG, "Skipping remainder of malformed SEI NAL unit.");
+ seiBuffer.setPosition(seiBuffer.limit());
+ } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) {
+ // Ignore country_code (1) + provider_code (2) + user_identifier (4)
+ // + user_data_type_code (1).
+ seiBuffer.skipBytes(8);
+ // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1).
+ int ccCount = seiBuffer.readUnsignedByte() & 0x1F;
+ // Ignore em_data (1)
+ seiBuffer.skipBytes(1);
+ // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
+ // + cc_data_1 (8) + cc_data_2 (8).
+ int sampleLength = ccCount * 3;
+ int sampleStartPosition = seiBuffer.getPosition();
+ for (TrackOutput output : outputs) {
+ seiBuffer.setPosition(sampleStartPosition);
+ output.sampleData(seiBuffer, sampleLength);
+ output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
+ }
+ // Ignore trailing information in SEI, if any.
+ seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
+ } else {
+ seiBuffer.skipBytes(payloadSize);
+ }
+ }
+ }
+
+ /**
+ * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a
+ * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the
+ * number of 0xFF bytes and T is the value of the terminating byte.
+ *
+ * @param buffer The buffer from which to read the value.
+ * @returns The read value, or -1 if the end of the buffer is reached before a value is read.
+ */
+ private static int readNon255TerminatedValue(ParsableByteArray buffer) {
+ int b;
+ int value = 0;
+ do {
+ if (buffer.bytesLeft() == 0) {
+ return -1;
+ }
+ b = buffer.readUnsignedByte();
+ value += b;
+ } while (b == 0xFF);
+ return value;
+ }
+
+ /**
+ * Inspects an sei message to determine whether it contains CEA-608.
+ * <p>
+ * The position of {@code payload} is left unchanged.
+ *
+ * @param payloadType The payload type of the message.
+ * @param payloadLength The length of the payload.
+ * @param payload A {@link ParsableByteArray} containing the payload.
+ * @return Whether the sei message contains CEA-608.
+ */
+ private static boolean isSeiMessageCea608(int payloadType, int payloadLength,
+ ParsableByteArray payload) {
+ if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) {
+ return false;
+ }
+ int startPosition = payload.getPosition();
+ int countryCode = payload.readUnsignedByte();
+ int providerCode = payload.readUnsignedShort();
+ int userIdentifier = payload.readInt();
+ int userDataTypeCode = payload.readUnsignedByte();
+ payload.setPosition(startPosition);
+ return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE
+ && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE;
+ }
+
+ private CeaUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.google.android.exoplayer2.text.dvb;
+
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for DVB Subtitles.
+ */
+public final class DvbDecoder extends SimpleSubtitleDecoder {
+
+ private final DvbParser parser;
+
+ /**
+ * @param initializationData The initialization data for the decoder. The initialization data
+ * must consist of a single byte array containing 5 bytes: flag_pes_stripped (1),
+ * composition_page (2), ancillary_page (2).
+ */
+ public DvbDecoder(List<byte[]> initializationData) {
+ super("DvbDecoder");
+ ParsableByteArray data = new ParsableByteArray(initializationData.get(0));
+ int subtitleCompositionPage = data.readUnsignedShort();
+ int subtitleAncillaryPage = data.readUnsignedShort();
+ parser = new DvbParser(subtitleCompositionPage, subtitleAncillaryPage);
+ }
+
+ @Override
+ protected DvbSubtitle decode(byte[] data, int length, boolean reset) {
+ if (reset) {
+ parser.reset();
+ }
+ return new DvbSubtitle(parser.decode(data, length));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/dvb/DvbParser.java
@@ -0,0 +1,1025 @@
+/*
+ * 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 com.google.android.exoplayer2.text.dvb;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Region;
+import android.util.Log;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses {@link Cue}s from a DVB subtitle bitstream.
+ */
+/* package */ final class DvbParser {
+
+ private static final String TAG = "DvbParser";
+
+ // Segment types, as defined by ETSI EN 300 743 Table 2
+ private static final int SEGMENT_TYPE_PAGE_COMPOSITION = 0x10;
+ private static final int SEGMENT_TYPE_REGION_COMPOSITION = 0x11;
+ private static final int SEGMENT_TYPE_CLUT_DEFINITION = 0x12;
+ private static final int SEGMENT_TYPE_OBJECT_DATA = 0x13;
+ private static final int SEGMENT_TYPE_DISPLAY_DEFINITION = 0x14;
+
+ // Page states, as defined by ETSI EN 300 743 Table 3
+ private static final int PAGE_STATE_NORMAL = 0; // Update. Only changed elements.
+ // private static final int PAGE_STATE_ACQUISITION = 1; // Refresh. All elements.
+ // private static final int PAGE_STATE_CHANGE = 2; // New. All elements.
+
+ // Region depths, as defined by ETSI EN 300 743 Table 5
+ // private static final int REGION_DEPTH_2_BIT = 1;
+ private static final int REGION_DEPTH_4_BIT = 2;
+ private static final int REGION_DEPTH_8_BIT = 3;
+
+ // Object codings, as defined by ETSI EN 300 743 Table 8
+ private static final int OBJECT_CODING_PIXELS = 0;
+ private static final int OBJECT_CODING_STRING = 1;
+
+ // Pixel-data types, as defined by ETSI EN 300 743 Table 9
+ private static final int DATA_TYPE_2BP_CODE_STRING = 0x10;
+ private static final int DATA_TYPE_4BP_CODE_STRING = 0x11;
+ private static final int DATA_TYPE_8BP_CODE_STRING = 0x12;
+ private static final int DATA_TYPE_24_TABLE_DATA = 0x20;
+ private static final int DATA_TYPE_28_TABLE_DATA = 0x21;
+ private static final int DATA_TYPE_48_TABLE_DATA = 0x22;
+ private static final int DATA_TYPE_END_LINE = 0xF0;
+
+ // Clut mapping tables, as defined by ETSI EN 300 743 10.4, 10.5, 10.6
+ private static final byte[] defaultMap2To4 = {
+ (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x0F};
+ private static final byte[] defaultMap2To8 = {
+ (byte) 0x00, (byte) 0x77, (byte) 0x88, (byte) 0xFF};
+ private static final byte[] defaultMap4To8 = {
+ (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33,
+ (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77,
+ (byte) 0x88, (byte) 0x99, (byte) 0xAA, (byte) 0xBB,
+ (byte) 0xCC, (byte) 0xDD, (byte) 0xEE, (byte) 0xFF};
+
+ private final Paint defaultPaint;
+ private final Paint fillRegionPaint;
+ private final Canvas canvas;
+ private final DisplayDefinition defaultDisplayDefinition;
+ private final ClutDefinition defaultClutDefinition;
+ private final SubtitleService subtitleService;
+
+ private Bitmap bitmap;
+
+ /**
+ * Construct an instance for the given subtitle and ancillary page ids.
+ *
+ * @param subtitlePageId The id of the subtitle page carrying the subtitle to be parsed.
+ * @param ancillaryPageId The id of the ancillary page containing additional data.
+ */
+ public DvbParser(int subtitlePageId, int ancillaryPageId) {
+ defaultPaint = new Paint();
+ defaultPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ defaultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ defaultPaint.setPathEffect(null);
+ fillRegionPaint = new Paint();
+ fillRegionPaint.setStyle(Paint.Style.FILL);
+ fillRegionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
+ fillRegionPaint.setPathEffect(null);
+ canvas = new Canvas();
+ defaultDisplayDefinition = new DisplayDefinition(719, 575, 0, 719, 0, 575);
+ defaultClutDefinition = new ClutDefinition(0, generateDefault2BitClutEntries(),
+ generateDefault4BitClutEntries(), generateDefault8BitClutEntries());
+ subtitleService = new SubtitleService(subtitlePageId, ancillaryPageId);
+ }
+
+ /**
+ * Resets the parser.
+ */
+ public void reset() {
+ subtitleService.reset();
+ }
+
+ /**
+ * Decodes a subtitling packet, returning a list of parsed {@link Cue}s.
+ *
+ * @param data The subtitling packet data to decode.
+ * @param limit The limit in {@code data} at which to stop decoding.
+ * @return The parsed {@link Cue}s.
+ */
+ public List<Cue> decode(byte[] data, int limit) {
+ // Parse the input data.
+ ParsableBitArray dataBitArray = new ParsableBitArray(data, limit);
+ while (dataBitArray.bitsLeft() >= 48 // sync_byte (8) + segment header (40)
+ && dataBitArray.readBits(8) == 0x0F) {
+ parseSubtitlingSegment(dataBitArray, subtitleService);
+ }
+
+ if (subtitleService.pageComposition == null) {
+ return Collections.emptyList();
+ }
+
+ // Update the canvas bitmap if necessary.
+ DisplayDefinition displayDefinition = subtitleService.displayDefinition != null
+ ? subtitleService.displayDefinition : defaultDisplayDefinition;
+ if (bitmap == null || displayDefinition.width + 1 != bitmap.getWidth()
+ || displayDefinition.height + 1 != bitmap.getHeight()) {
+ bitmap = Bitmap.createBitmap(displayDefinition.width + 1, displayDefinition.height + 1,
+ Bitmap.Config.ARGB_8888);
+ canvas.setBitmap(bitmap);
+ }
+
+ // Build the cues.
+ List<Cue> cues = new ArrayList<>();
+ SparseArray<PageRegion> pageRegions = subtitleService.pageComposition.regions;
+ for (int i = 0; i < pageRegions.size(); i++) {
+ PageRegion pageRegion = pageRegions.valueAt(i);
+ int regionId = pageRegions.keyAt(i);
+ RegionComposition regionComposition = subtitleService.regions.get(regionId);
+
+ // Clip drawing to the current region and display definition window.
+ int baseHorizontalAddress = pageRegion.horizontalAddress
+ + displayDefinition.horizontalPositionMinimum;
+ int baseVerticalAddress = pageRegion.verticalAddress
+ + displayDefinition.verticalPositionMinimum;
+ int clipRight = Math.min(baseHorizontalAddress + regionComposition.width,
+ displayDefinition.horizontalPositionMaximum);
+ int clipBottom = Math.min(baseVerticalAddress + regionComposition.height,
+ displayDefinition.verticalPositionMaximum);
+ canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom,
+ Region.Op.REPLACE);
+
+ ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);
+ if (clutDefinition == null) {
+ clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);
+ if (clutDefinition == null) {
+ clutDefinition = defaultClutDefinition;
+ }
+ }
+
+ SparseArray<RegionObject> regionObjects = regionComposition.regionObjects;
+ for (int j = 0; j < regionObjects.size(); j++) {
+ int objectId = regionObjects.keyAt(j);
+ RegionObject regionObject = regionObjects.valueAt(j);
+ ObjectData objectData = subtitleService.objects.get(objectId);
+ if (objectData == null) {
+ objectData = subtitleService.ancillaryObjects.get(objectId);
+ }
+ if (objectData != null) {
+ Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint;
+ paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth,
+ baseHorizontalAddress + regionObject.horizontalPosition,
+ baseVerticalAddress + regionObject.verticalPosition, paint, canvas);
+ }
+ }
+
+ if (regionComposition.fillFlag) {
+ int color;
+ if (regionComposition.depth == REGION_DEPTH_8_BIT) {
+ color = clutDefinition.clutEntries8Bit[regionComposition.pixelCode8Bit];
+ } else if (regionComposition.depth == REGION_DEPTH_4_BIT) {
+ color = clutDefinition.clutEntries4Bit[regionComposition.pixelCode4Bit];
+ } else {
+ color = clutDefinition.clutEntries2Bit[regionComposition.pixelCode2Bit];
+ }
+ fillRegionPaint.setColor(color);
+ canvas.drawRect(baseHorizontalAddress, baseVerticalAddress,
+ baseHorizontalAddress + regionComposition.width,
+ baseVerticalAddress + regionComposition.height,
+ fillRegionPaint);
+ }
+
+ Bitmap cueBitmap = Bitmap.createBitmap(bitmap, baseHorizontalAddress, baseVerticalAddress,
+ regionComposition.width, regionComposition.height);
+ cues.add(new Cue(cueBitmap, (float) baseHorizontalAddress / displayDefinition.width,
+ Cue.ANCHOR_TYPE_START, (float) baseVerticalAddress / displayDefinition.height,
+ Cue.ANCHOR_TYPE_START, (float) regionComposition.width / displayDefinition.width,
+ (float) regionComposition.height / displayDefinition.height));
+
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
+ }
+
+ return cues;
+ }
+
+ // Static parsing.
+
+ /**
+ * Parses a subtitling segment, as defined by ETSI EN 300 743 7.2
+ * <p>
+ * The {@link SubtitleService} is updated with the parsed segment data.
+ */
+ private static void parseSubtitlingSegment(ParsableBitArray data, SubtitleService service) {
+ int segmentType = data.readBits(8);
+ int pageId = data.readBits(16);
+ int dataFieldLength = data.readBits(16);
+ int dataFieldLimit = data.getBytePosition() + dataFieldLength;
+
+ if ((dataFieldLength * 8) > data.bitsLeft()) {
+ Log.w(TAG, "Data field length exceeds limit");
+ // Skip to the very end.
+ data.skipBits(data.bitsLeft());
+ return;
+ }
+
+ switch (segmentType) {
+ case SEGMENT_TYPE_DISPLAY_DEFINITION:
+ if (pageId == service.subtitlePageId) {
+ service.displayDefinition = parseDisplayDefinition(data);
+ }
+ break;
+ case SEGMENT_TYPE_PAGE_COMPOSITION:
+ if (pageId == service.subtitlePageId) {
+ PageComposition current = service.pageComposition;
+ PageComposition pageComposition = parsePageComposition(data, dataFieldLength);
+ if (pageComposition.state != PAGE_STATE_NORMAL) {
+ service.pageComposition = pageComposition;
+ service.regions.clear();
+ service.cluts.clear();
+ service.objects.clear();
+ } else if (current != null && current.version != pageComposition.version) {
+ service.pageComposition = pageComposition;
+ }
+ }
+ break;
+ case SEGMENT_TYPE_REGION_COMPOSITION:
+ PageComposition pageComposition = service.pageComposition;
+ if (pageId == service.subtitlePageId && pageComposition != null) {
+ RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength);
+ if (pageComposition.state == PAGE_STATE_NORMAL) {
+ regionComposition.mergeFrom(service.regions.get(regionComposition.id));
+ }
+ service.regions.put(regionComposition.id, regionComposition);
+ }
+ break;
+ case SEGMENT_TYPE_CLUT_DEFINITION:
+ if (pageId == service.subtitlePageId) {
+ ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
+ service.cluts.put(clutDefinition.id, clutDefinition);
+ } else if (pageId == service.ancillaryPageId) {
+ ClutDefinition clutDefinition = parseClutDefinition(data, dataFieldLength);
+ service.ancillaryCluts.put(clutDefinition.id, clutDefinition);
+ }
+ break;
+ case SEGMENT_TYPE_OBJECT_DATA:
+ if (pageId == service.subtitlePageId) {
+ ObjectData objectData = parseObjectData(data);
+ service.objects.put(objectData.id, objectData);
+ } else if (pageId == service.ancillaryPageId) {
+ ObjectData objectData = parseObjectData(data);
+ service.ancillaryObjects.put(objectData.id, objectData);
+ }
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+
+ // Skip to the next segment.
+ data.skipBytes(dataFieldLimit - data.getBytePosition());
+ }
+
+ /**
+ * Parses a display definition segment, as defined by ETSI EN 300 743 7.2.1.
+ */
+ private static DisplayDefinition parseDisplayDefinition(ParsableBitArray data) {
+ data.skipBits(4); // dds_version_number (4).
+ boolean displayWindowFlag = data.readBit();
+ data.skipBits(3); // Skip reserved.
+ int width = data.readBits(16);
+ int height = data.readBits(16);
+
+ int horizontalPositionMinimum;
+ int horizontalPositionMaximum;
+ int verticalPositionMinimum;
+ int verticalPositionMaximum;
+ if (displayWindowFlag) {
+ horizontalPositionMinimum = data.readBits(16);
+ horizontalPositionMaximum = data.readBits(16);
+ verticalPositionMinimum = data.readBits(16);
+ verticalPositionMaximum = data.readBits(16);
+ } else {
+ horizontalPositionMinimum = 0;
+ horizontalPositionMaximum = width;
+ verticalPositionMinimum = 0;
+ verticalPositionMaximum = height;
+ }
+
+ return new DisplayDefinition(width, height, horizontalPositionMinimum,
+ horizontalPositionMaximum, verticalPositionMinimum, verticalPositionMaximum);
+ }
+
+ /**
+ * Parses a page composition segment, as defined by ETSI EN 300 743 7.2.2.
+ */
+ private static PageComposition parsePageComposition(ParsableBitArray data, int length) {
+ int timeoutSecs = data.readBits(8);
+ int version = data.readBits(4);
+ int state = data.readBits(2);
+ data.skipBits(2);
+ int remainingLength = length - 2;
+
+ SparseArray<PageRegion> regions = new SparseArray<>();
+ while (remainingLength > 0) {
+ int regionId = data.readBits(8);
+ data.skipBits(8); // Skip reserved.
+ int regionHorizontalAddress = data.readBits(16);
+ int regionVerticalAddress = data.readBits(16);
+ remainingLength -= 6;
+ regions.put(regionId, new PageRegion(regionHorizontalAddress, regionVerticalAddress));
+ }
+
+ return new PageComposition(timeoutSecs, version, state, regions);
+ }
+
+ /**
+ * Parses a region composition segment, as defined by ETSI EN 300 743 7.2.3.
+ */
+ private static RegionComposition parseRegionComposition(ParsableBitArray data, int length) {
+ int id = data.readBits(8);
+ data.skipBits(4); // Skip region_version_number
+ boolean fillFlag = data.readBit();
+ data.skipBits(3); // Skip reserved.
+ int width = data.readBits(16);
+ int height = data.readBits(16);
+ int levelOfCompatibility = data.readBits(3);
+ int depth = data.readBits(3);
+ data.skipBits(2); // Skip reserved.
+ int clutId = data.readBits(8);
+ int pixelCode8Bit = data.readBits(8);
+ int pixelCode4Bit = data.readBits(4);
+ int pixelCode2Bit = data.readBits(2);
+ data.skipBits(2); // Skip reserved
+ int remainingLength = length - 10;
+
+ SparseArray<RegionObject> regionObjects = new SparseArray<>();
+ while (remainingLength > 0) {
+ int objectId = data.readBits(16);
+ int objectType = data.readBits(2);
+ int objectProvider = data.readBits(2);
+ int objectHorizontalPosition = data.readBits(12);
+ data.skipBits(4); // Skip reserved.
+ int objectVerticalPosition = data.readBits(12);
+ remainingLength -= 6;
+
+ int foregroundPixelCode = 0;
+ int backgroundPixelCode = 0;
+ if (objectType == 0x01 || objectType == 0x02) { // Only seems to affect to char subtitles.
+ foregroundPixelCode = data.readBits(8);
+ backgroundPixelCode = data.readBits(8);
+ remainingLength -= 2;
+ }
+
+ regionObjects.put(objectId, new RegionObject(objectType, objectProvider,
+ objectHorizontalPosition, objectVerticalPosition, foregroundPixelCode,
+ backgroundPixelCode));
+ }
+
+ return new RegionComposition(id, fillFlag, width, height, levelOfCompatibility, depth, clutId,
+ pixelCode8Bit, pixelCode4Bit, pixelCode2Bit, regionObjects);
+ }
+
+ /**
+ * Parses a CLUT definition segment, as defined by ETSI EN 300 743 7.2.4.
+ */
+ private static ClutDefinition parseClutDefinition(ParsableBitArray data, int length) {
+ int clutId = data.readBits(8);
+ data.skipBits(8); // Skip clut_version_number (4), reserved (4)
+ int remainingLength = length - 2;
+
+ int[] clutEntries2Bit = generateDefault2BitClutEntries();
+ int[] clutEntries4Bit = generateDefault4BitClutEntries();
+ int[] clutEntries8Bit = generateDefault8BitClutEntries();
+
+ while (remainingLength > 0) {
+ int entryId = data.readBits(8);
+ int entryFlags = data.readBits(8);
+ remainingLength -= 2;
+
+ int[] clutEntries;
+ if ((entryFlags & 0x80) != 0) {
+ clutEntries = clutEntries2Bit;
+ } else if ((entryFlags & 0x40) != 0) {
+ clutEntries = clutEntries4Bit;
+ } else {
+ clutEntries = clutEntries8Bit;
+ }
+
+ int y;
+ int cr;
+ int cb;
+ int t;
+ if ((entryFlags & 0x01) != 0) {
+ y = data.readBits(8);
+ cr = data.readBits(8);
+ cb = data.readBits(8);
+ t = data.readBits(8);
+ remainingLength -= 4;
+ } else {
+ y = data.readBits(6) << 2;
+ cr = data.readBits(4) << 4;
+ cb = data.readBits(4) << 4;
+ t = data.readBits(2) << 6;
+ remainingLength -= 2;
+ }
+
+ if (y == 0x00) {
+ cr = 0x00;
+ cb = 0x00;
+ t = 0xFF;
+ }
+
+ int a = (byte) (0xFF - (t & 0xFF));
+ int r = (int) (y + (1.40200 * (cr - 128)));
+ int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128)));
+ int b = (int) (y + (1.77200 * (cb - 128)));
+ clutEntries[entryId] = getColor(a, Util.constrainValue(r, 0, 255),
+ Util.constrainValue(g, 0, 255), Util.constrainValue(b, 0, 255));
+ }
+
+ return new ClutDefinition(clutId, clutEntries2Bit, clutEntries4Bit, clutEntries8Bit);
+ }
+
+ /**
+ * Parses an object data segment, as defined by ETSI EN 300 743 7.2.5.
+ *
+ * @return The parsed object data.
+ */
+ private static ObjectData parseObjectData(ParsableBitArray data) {
+ int objectId = data.readBits(16);
+ data.skipBits(4); // Skip object_version_number
+ int objectCodingMethod = data.readBits(2);
+ boolean nonModifyingColorFlag = data.readBit();
+ data.skipBits(1); // Skip reserved.
+
+ byte[] topFieldData = null;
+ byte[] bottomFieldData = null;
+
+ if (objectCodingMethod == OBJECT_CODING_STRING) {
+ int numberOfCodes = data.readBits(8);
+ // TODO: Parse and use character_codes.
+ data.skipBits(numberOfCodes * 16); // Skip character_codes.
+ } else if (objectCodingMethod == OBJECT_CODING_PIXELS) {
+ int topFieldDataLength = data.readBits(16);
+ int bottomFieldDataLength = data.readBits(16);
+ if (topFieldDataLength > 0) {
+ topFieldData = new byte[topFieldDataLength];
+ data.readBytes(topFieldData, 0, topFieldDataLength);
+ }
+ if (bottomFieldDataLength > 0) {
+ bottomFieldData = new byte[bottomFieldDataLength];
+ data.readBytes(bottomFieldData, 0, bottomFieldDataLength);
+ } else {
+ bottomFieldData = topFieldData;
+ }
+ }
+
+ return new ObjectData(objectId, nonModifyingColorFlag, topFieldData, bottomFieldData);
+ }
+
+ private static int[] generateDefault2BitClutEntries() {
+ int[] entries = new int[4];
+ entries[0] = 0x00000000;
+ entries[1] = 0xFFFFFFFF;
+ entries[2] = 0xFF000000;
+ entries[3] = 0xFF7F7F7F;
+ return entries;
+ }
+
+ private static int[] generateDefault4BitClutEntries() {
+ int[] entries = new int[16];
+ entries[0] = 0x00000000;
+ for (int i = 1; i < entries.length; i++) {
+ if (i < 8) {
+ entries[i] = getColor(
+ 0xFF,
+ ((i & 0x01) != 0 ? 0xFF : 0x00),
+ ((i & 0x02) != 0 ? 0xFF : 0x00),
+ ((i & 0x04) != 0 ? 0xFF : 0x00));
+ } else {
+ entries[i] = getColor(
+ 0xFF,
+ ((i & 0x01) != 0 ? 0x7F : 0x00),
+ ((i & 0x02) != 0 ? 0x7F : 0x00),
+ ((i & 0x04) != 0 ? 0x7F : 0x00));
+ }
+ }
+ return entries;
+ }
+
+ private static int[] generateDefault8BitClutEntries() {
+ int[] entries = new int[256];
+ entries[0] = 0x00000000;
+ for (int i = 0; i < entries.length; i++) {
+ if (i < 8) {
+ entries[i] = getColor(
+ 0x3F,
+ ((i & 0x01) != 0 ? 0xFF : 0x00),
+ ((i & 0x02) != 0 ? 0xFF : 0x00),
+ ((i & 0x04) != 0 ? 0xFF : 0x00));
+ } else {
+ switch (i & 0x88) {
+ case 0x00:
+ entries[i] = getColor(
+ 0xFF,
+ (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
+ (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
+ (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
+ break;
+ case 0x08:
+ entries[i] = getColor(
+ 0x7F,
+ (((i & 0x01) != 0 ? 0x55 : 0x00) + ((i & 0x10) != 0 ? 0xAA : 0x00)),
+ (((i & 0x02) != 0 ? 0x55 : 0x00) + ((i & 0x20) != 0 ? 0xAA : 0x00)),
+ (((i & 0x04) != 0 ? 0x55 : 0x00) + ((i & 0x40) != 0 ? 0xAA : 0x00)));
+ break;
+ case 0x80:
+ entries[i] = getColor(
+ 0xFF,
+ (127 + ((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
+ (127 + ((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
+ (127 + ((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
+ break;
+ case 0x88:
+ entries[i] = getColor(
+ 0xFF,
+ (((i & 0x01) != 0 ? 0x2B : 0x00) + ((i & 0x10) != 0 ? 0x55 : 0x00)),
+ (((i & 0x02) != 0 ? 0x2B : 0x00) + ((i & 0x20) != 0 ? 0x55 : 0x00)),
+ (((i & 0x04) != 0 ? 0x2B : 0x00) + ((i & 0x40) != 0 ? 0x55 : 0x00)));
+ break;
+ }
+ }
+ }
+ return entries;
+ }
+
+ private static int getColor(int a, int r, int g, int b) {
+ return (a << 24) | (r << 16) | (g << 8) | b;
+ }
+
+ // Static drawing.
+
+ /**
+ * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas.
+ */
+ private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition,
+ int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) {
+ int[] clutEntries;
+ if (regionDepth == REGION_DEPTH_8_BIT) {
+ clutEntries = clutDefinition.clutEntries8Bit;
+ } else if (regionDepth == REGION_DEPTH_4_BIT) {
+ clutEntries = clutDefinition.clutEntries4Bit;
+ } else {
+ clutEntries = clutDefinition.clutEntries2Bit;
+ }
+ paintPixelDataSubBlock(objectData.topFieldData, clutEntries, regionDepth, horizontalAddress,
+ verticalAddress, paint, canvas);
+ paintPixelDataSubBlock(objectData.bottomFieldData, clutEntries, regionDepth, horizontalAddress,
+ verticalAddress + 1, paint, canvas);
+ }
+
+ /**
+ * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas.
+ */
+ private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth,
+ int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) {
+ ParsableBitArray data = new ParsableBitArray(pixelData);
+ int column = horizontalAddress;
+ int line = verticalAddress;
+ byte[] clutMapTable2To4 = null;
+ byte[] clutMapTable2To8 = null;
+ byte[] clutMapTable4To8 = null;
+
+ while (data.bitsLeft() != 0) {
+ int dataType = data.readBits(8);
+ switch (dataType) {
+ case DATA_TYPE_2BP_CODE_STRING:
+ byte[] clutMapTable2ToX;
+ if (regionDepth == REGION_DEPTH_8_BIT) {
+ clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8;
+ } else if (regionDepth == REGION_DEPTH_4_BIT) {
+ clutMapTable2ToX = clutMapTable2To4 == null ? defaultMap2To4 : clutMapTable2To4;
+ } else {
+ clutMapTable2ToX = null;
+ }
+ column = paint2BitPixelCodeString(data, clutEntries, clutMapTable2ToX, column, line,
+ paint, canvas);
+ data.byteAlign();
+ break;
+ case DATA_TYPE_4BP_CODE_STRING:
+ byte[] clutMapTable4ToX;
+ if (regionDepth == REGION_DEPTH_8_BIT) {
+ clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8;
+ } else {
+ clutMapTable4ToX = null;
+ }
+ column = paint4BitPixelCodeString(data, clutEntries, clutMapTable4ToX, column, line,
+ paint, canvas);
+ data.byteAlign();
+ break;
+ case DATA_TYPE_8BP_CODE_STRING:
+ column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas);
+ break;
+ case DATA_TYPE_24_TABLE_DATA:
+ clutMapTable2To4 = buildClutMapTable(4, 4, data);
+ break;
+ case DATA_TYPE_28_TABLE_DATA:
+ clutMapTable2To8 = buildClutMapTable(4, 8, data);
+ break;
+ case DATA_TYPE_48_TABLE_DATA:
+ clutMapTable2To8 = buildClutMapTable(16, 8, data);
+ break;
+ case DATA_TYPE_END_LINE:
+ column = horizontalAddress;
+ line += 2;
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+ }
+
+ /**
+ * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.
+ */
+ private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries,
+ byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) {
+ boolean endOfPixelCodeString = false;
+ do {
+ int runLength = 0;
+ int clutIndex = 0;
+ int peek = data.readBits(2);
+ if (!data.readBit()) {
+ runLength = 1;
+ clutIndex = peek;
+ } else if (data.readBit()) {
+ runLength = 3 + data.readBits(3);
+ clutIndex = data.readBits(2);
+ } else if (!data.readBit()) {
+ switch (data.readBits(2)) {
+ case 0x00:
+ endOfPixelCodeString = true;
+ break;
+ case 0x01:
+ runLength = 2;
+ break;
+ case 0x02:
+ runLength = 12 + data.readBits(4);
+ clutIndex = data.readBits(2);
+ break;
+ case 0x03:
+ runLength = 29 + data.readBits(8);
+ clutIndex = data.readBits(2);
+ break;
+ }
+ }
+
+ if (runLength != 0 && paint != null) {
+ paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
+ canvas.drawRect(column, line, column + runLength, line + 1, paint);
+ }
+
+ column += runLength;
+ } while (!endOfPixelCodeString);
+
+ return column;
+ }
+
+ /**
+ * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.
+ */
+ private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries,
+ byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) {
+ boolean endOfPixelCodeString = false;
+ do {
+ int runLength = 0;
+ int clutIndex = 0;
+ int peek = data.readBits(4);
+ if (peek != 0x00) {
+ runLength = 1;
+ clutIndex = peek;
+ } else if (!data.readBit()) {
+ peek = data.readBits(3);
+ if (peek != 0x00) {
+ runLength = 2 + peek;
+ clutIndex = 0x00;
+ } else {
+ endOfPixelCodeString = true;
+ }
+ } else if (!data.readBit()) {
+ runLength = 4 + data.readBits(2);
+ clutIndex = data.readBits(4);
+ } else {
+ switch (data.readBits(2)) {
+ case 0x00:
+ runLength = 1;
+ break;
+ case 0x01:
+ runLength = 2;
+ break;
+ case 0x02:
+ runLength = 9 + data.readBits(4);
+ clutIndex = data.readBits(4);
+ break;
+ case 0x03:
+ runLength = 25 + data.readBits(8);
+ clutIndex = data.readBits(4);
+ break;
+ }
+ }
+
+ if (runLength != 0 && paint != null) {
+ paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
+ canvas.drawRect(column, line, column + runLength, line + 1, paint);
+ }
+
+ column += runLength;
+ } while (!endOfPixelCodeString);
+
+ return column;
+ }
+
+ /**
+ * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.
+ */
+ private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries,
+ byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) {
+ boolean endOfPixelCodeString = false;
+ do {
+ int runLength = 0;
+ int clutIndex = 0;
+ int peek = data.readBits(8);
+ if (peek != 0x00) {
+ runLength = 1;
+ clutIndex = peek;
+ } else {
+ if (!data.readBit()) {
+ peek = data.readBits(7);
+ if (peek != 0x00) {
+ runLength = peek;
+ clutIndex = 0x00;
+ } else {
+ endOfPixelCodeString = true;
+ }
+ } else {
+ runLength = data.readBits(7);
+ clutIndex = data.readBits(8);
+ }
+ }
+
+ if (runLength != 0 && paint != null) {
+ paint.setColor(clutEntries[clutMapTable != null ? clutMapTable[clutIndex] : clutIndex]);
+ canvas.drawRect(column, line, column + runLength, line + 1, paint);
+ }
+ column += runLength;
+ } while (!endOfPixelCodeString);
+
+ return column;
+ }
+
+ private static byte[] buildClutMapTable(int length, int bitsPerEntry, ParsableBitArray data) {
+ byte[] clutMapTable = new byte[length];
+ for (int i = 0; i < length; i++) {
+ clutMapTable[i] = (byte) data.readBits(bitsPerEntry);
+ }
+ return clutMapTable;
+ }
+
+ // Private inner classes.
+
+ /**
+ * The subtitle service definition.
+ */
+ private static final class SubtitleService {
+
+ public final int subtitlePageId;
+ public final int ancillaryPageId;
+
+ public final SparseArray<RegionComposition> regions = new SparseArray<>();
+ public final SparseArray<ClutDefinition> cluts = new SparseArray<>();
+ public final SparseArray<ObjectData> objects = new SparseArray<>();
+ public final SparseArray<ClutDefinition> ancillaryCluts = new SparseArray<>();
+ public final SparseArray<ObjectData> ancillaryObjects = new SparseArray<>();
+
+ public DisplayDefinition displayDefinition;
+ public PageComposition pageComposition;
+
+ public SubtitleService(int subtitlePageId, int ancillaryPageId) {
+ this.subtitlePageId = subtitlePageId;
+ this.ancillaryPageId = ancillaryPageId;
+ }
+
+ public void reset() {
+ regions.clear();
+ cluts.clear();
+ objects.clear();
+ ancillaryCluts.clear();
+ ancillaryObjects.clear();
+ displayDefinition = null;
+ pageComposition = null;
+ }
+
+ }
+
+ /**
+ * Contains the geometry and active area of the subtitle service.
+ * <p>
+ * See ETSI EN 300 743 7.2.1
+ */
+ private static final class DisplayDefinition {
+
+ public final int width;
+ public final int height;
+
+ public final int horizontalPositionMinimum;
+ public final int horizontalPositionMaximum;
+ public final int verticalPositionMinimum;
+ public final int verticalPositionMaximum;
+
+ public DisplayDefinition(int width, int height, int horizontalPositionMinimum,
+ int horizontalPositionMaximum, int verticalPositionMinimum, int verticalPositionMaximum) {
+ this.width = width;
+ this.height = height;
+ this.horizontalPositionMinimum = horizontalPositionMinimum;
+ this.horizontalPositionMaximum = horizontalPositionMaximum;
+ this.verticalPositionMinimum = verticalPositionMinimum;
+ this.verticalPositionMaximum = verticalPositionMaximum;
+ }
+
+ }
+
+ /**
+ * The page is the definition and arrangement of regions in the screen.
+ * <p>
+ * See ETSI EN 300 743 7.2.2
+ */
+ private static final class PageComposition {
+
+ public final int timeOutSecs; // TODO: Use this or remove it.
+ public final int version;
+ public final int state;
+ public final SparseArray<PageRegion> regions;
+
+ public PageComposition(int timeoutSecs, int version, int state,
+ SparseArray<PageRegion> regions) {
+ this.timeOutSecs = timeoutSecs;
+ this.version = version;
+ this.state = state;
+ this.regions = regions;
+ }
+
+ }
+
+ /**
+ * A region within a {@link PageComposition}.
+ * <p>
+ * See ETSI EN 300 743 7.2.2
+ */
+ private static final class PageRegion {
+
+ public final int horizontalAddress;
+ public final int verticalAddress;
+
+ public PageRegion(int horizontalAddress, int verticalAddress) {
+ this.horizontalAddress = horizontalAddress;
+ this.verticalAddress = verticalAddress;
+ }
+
+ }
+
+ /**
+ * An area of the page composed of a list of objects and a CLUT.
+ * <p>
+ * See ETSI EN 300 743 7.2.3
+ */
+ private static final class RegionComposition {
+
+ public final int id;
+ public final boolean fillFlag;
+ public final int width;
+ public final int height;
+ public final int levelOfCompatibility; // TODO: Use this or remove it.
+ public final int depth;
+ public final int clutId;
+ public final int pixelCode8Bit;
+ public final int pixelCode4Bit;
+ public final int pixelCode2Bit;
+ public final SparseArray<RegionObject> regionObjects;
+
+ public RegionComposition(int id, boolean fillFlag, int width, int height,
+ int levelOfCompatibility, int depth, int clutId, int pixelCode8Bit, int pixelCode4Bit,
+ int pixelCode2Bit, SparseArray<RegionObject> regionObjects) {
+ this.id = id;
+ this.fillFlag = fillFlag;
+ this.width = width;
+ this.height = height;
+ this.levelOfCompatibility = levelOfCompatibility;
+ this.depth = depth;
+ this.clutId = clutId;
+ this.pixelCode8Bit = pixelCode8Bit;
+ this.pixelCode4Bit = pixelCode4Bit;
+ this.pixelCode2Bit = pixelCode2Bit;
+ this.regionObjects = regionObjects;
+ }
+
+ public void mergeFrom(RegionComposition otherRegionComposition) {
+ if (otherRegionComposition == null) {
+ return;
+ }
+ SparseArray<RegionObject> otherRegionObjects = otherRegionComposition.regionObjects;
+ for (int i = 0; i < otherRegionObjects.size(); i++) {
+ regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i));
+ }
+ }
+
+ }
+
+ /**
+ * An object within a {@link RegionComposition}.
+ * <p>
+ * See ETSI EN 300 743 7.2.3
+ */
+ private static final class RegionObject {
+
+ public final int type; // TODO: Use this or remove it.
+ public final int provider; // TODO: Use this or remove it.
+ public final int horizontalPosition;
+ public final int verticalPosition;
+ public final int foregroundPixelCode; // TODO: Use this or remove it.
+ public final int backgroundPixelCode; // TODO: Use this or remove it.
+
+ public RegionObject(int type, int provider, int horizontalPosition,
+ int verticalPosition, int foregroundPixelCode, int backgroundPixelCode) {
+ this.type = type;
+ this.provider = provider;
+ this.horizontalPosition = horizontalPosition;
+ this.verticalPosition = verticalPosition;
+ this.foregroundPixelCode = foregroundPixelCode;
+ this.backgroundPixelCode = backgroundPixelCode;
+ }
+
+ }
+
+ /**
+ * CLUT family definition containing the color tables for the three bit depths defined
+ * <p>
+ * See ETSI EN 300 743 7.2.4
+ */
+ private static final class ClutDefinition {
+
+ public final int id;
+ public final int[] clutEntries2Bit;
+ public final int[] clutEntries4Bit;
+ public final int[] clutEntries8Bit;
+
+ public ClutDefinition(int id, int[] clutEntries2Bit, int[] clutEntries4Bit,
+ int[] clutEntries8bit) {
+ this.id = id;
+ this.clutEntries2Bit = clutEntries2Bit;
+ this.clutEntries4Bit = clutEntries4Bit;
+ this.clutEntries8Bit = clutEntries8bit;
+ }
+
+ }
+
+ /**
+ * The textual or graphical representation of an object.
+ * <p>
+ * See ETSI EN 300 743 7.2.5
+ */
+ private static final class ObjectData {
+
+ public final int id;
+ public final boolean nonModifyingColorFlag;
+ public final byte[] topFieldData;
+ public final byte[] bottomFieldData;
+
+ public ObjectData(int id, boolean nonModifyingColorFlag, byte[] topFieldData,
+ byte[] bottomFieldData) {
+ this.id = id;
+ this.nonModifyingColorFlag = nonModifyingColorFlag;
+ this.topFieldData = topFieldData;
+ this.bottomFieldData = bottomFieldData;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/dvb/DvbSubtitle.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.google.android.exoplayer2.text.dvb;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import java.util.List;
+
+/**
+ * A representation of a DVB subtitle.
+ */
+/* package */ final class DvbSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ public DvbSubtitle(List<Cue> cues) {
+ this.cues = cues;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return cues;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
@@ -0,0 +1,114 @@
+/*
+ * 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 com.google.android.exoplayer2.text.subrip;
+
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Log;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.util.LongArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for SubRip.
+ */
+public final class SubripDecoder extends SimpleSubtitleDecoder {
+
+ private static final String TAG = "SubripDecoder";
+
+ private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)";
+ private static final Pattern SUBRIP_TIMING_LINE =
+ Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")?\\s*");
+
+ private final StringBuilder textBuilder;
+
+ public SubripDecoder() {
+ super("SubripDecoder");
+ textBuilder = new StringBuilder();
+ }
+
+ @Override
+ protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) {
+ ArrayList<Cue> cues = new ArrayList<>();
+ LongArray cueTimesUs = new LongArray();
+ ParsableByteArray subripData = new ParsableByteArray(bytes, length);
+ String currentLine;
+
+ while ((currentLine = subripData.readLine()) != null) {
+ if (currentLine.length() == 0) {
+ // Skip blank lines.
+ continue;
+ }
+
+ // Parse the index line as a sanity check.
+ try {
+ Integer.parseInt(currentLine);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping invalid index: " + currentLine);
+ continue;
+ }
+
+ // Read and parse the timing line.
+ boolean haveEndTimecode = false;
+ currentLine = subripData.readLine();
+ Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
+ if (matcher.matches()) {
+ cueTimesUs.add(parseTimecode(matcher, 1));
+ if (!TextUtils.isEmpty(matcher.group(6))) {
+ haveEndTimecode = true;
+ cueTimesUs.add(parseTimecode(matcher, 6));
+ }
+ } else {
+ Log.w(TAG, "Skipping invalid timing: " + currentLine);
+ continue;
+ }
+
+ // Read and parse the text.
+ textBuilder.setLength(0);
+ while (!TextUtils.isEmpty(currentLine = subripData.readLine())) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append("<br>");
+ }
+ textBuilder.append(currentLine.trim());
+ }
+
+ Spanned text = Html.fromHtml(textBuilder.toString());
+ cues.add(new Cue(text));
+ if (haveEndTimecode) {
+ cues.add(null);
+ }
+ }
+
+ Cue[] cuesArray = new Cue[cues.size()];
+ cues.toArray(cuesArray);
+ long[] cueTimesUsArray = cueTimesUs.toArray();
+ return new SubripSubtitle(cuesArray, cueTimesUsArray);
+ }
+
+ private static long parseTimecode(Matcher matcher, int groupOffset) {
+ long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000;
+ timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000;
+ timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000;
+ timestampMs += Long.parseLong(matcher.group(groupOffset + 4));
+ return timestampMs * 1000;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.google.android.exoplayer2.text.subrip;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a SubRip subtitle.
+ */
+/* package */ final class SubripSubtitle implements Subtitle {
+
+ private final Cue[] cues;
+ private final long[] cueTimesUs;
+
+ /**
+ * @param cues The cues in the subtitle. Null entries may be used to represent empty cues.
+ * @param cueTimesUs The cue times, in microseconds.
+ */
+ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
+ this.cues = cues;
+ this.cueTimesUs = cueTimesUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
+ return index < cueTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return cueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < cueTimesUs.length);
+ return cueTimesUs[index];
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
+ if (index == -1 || cues[index] == null) {
+ // timeUs is earlier than the start of the first cue, or we have an empty cue.
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(cues[index]);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -0,0 +1,547 @@
+/*
+ * 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 com.google.android.exoplayer2.text.ttml;
+
+import android.text.Layout;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ColorParser;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.util.XmlPullParserUtil;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
+ * supported by this decoder are:
+ * <ul>
+ * <li>content
+ * <li>core
+ * <li>presentation
+ * <li>profile
+ * <li>structure
+ * <li>time-offset
+ * <li>timing
+ * <li>tickRate
+ * <li>time-clock-with-frames
+ * <li>time-clock
+ * <li>time-offset-with-frames
+ * <li>time-offset-with-ticks
+ * </ul>
+ * @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
+ */
+public final class TtmlDecoder extends SimpleSubtitleDecoder {
+
+ private static final String TAG = "TtmlDecoder";
+
+ private static final String TTP = "http://www.w3.org/ns/ttml#parameter";
+
+ private static final String ATTR_BEGIN = "begin";
+ private static final String ATTR_DURATION = "dur";
+ private static final String ATTR_END = "end";
+ private static final String ATTR_STYLE = "style";
+ private static final String ATTR_REGION = "region";
+
+ private static final Pattern CLOCK_TIME =
+ Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
+ + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
+ private static final Pattern OFFSET_TIME =
+ Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
+ private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
+ private static final Pattern PERCENTAGE_COORDINATES =
+ Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
+
+ private static final int DEFAULT_FRAME_RATE = 30;
+
+ private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
+ new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
+
+ private final XmlPullParserFactory xmlParserFactory;
+
+ public TtmlDecoder() {
+ super("TtmlDecoder");
+ try {
+ xmlParserFactory = XmlPullParserFactory.newInstance();
+ xmlParserFactory.setNamespaceAware(true);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+ }
+ }
+
+ @Override
+ protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ try {
+ XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+ Map<String, TtmlStyle> globalStyles = new HashMap<>();
+ Map<String, TtmlRegion> regionMap = new HashMap<>();
+ regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion());
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
+ xmlParser.setInput(inputStream, null);
+ TtmlSubtitle ttmlSubtitle = null;
+ LinkedList<TtmlNode> nodeStack = new LinkedList<>();
+ int unsupportedNodeDepth = 0;
+ int eventType = xmlParser.getEventType();
+ FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ TtmlNode parent = nodeStack.peekLast();
+ if (unsupportedNodeDepth == 0) {
+ String name = xmlParser.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (TtmlNode.TAG_TT.equals(name)) {
+ frameAndTickRate = parseFrameAndTickRates(xmlParser);
+ }
+ if (!isSupportedTag(name)) {
+ Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
+ unsupportedNodeDepth++;
+ } else if (TtmlNode.TAG_HEAD.equals(name)) {
+ parseHeader(xmlParser, globalStyles, regionMap);
+ } else {
+ try {
+ TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
+ nodeStack.addLast(node);
+ if (parent != null) {
+ parent.addChild(node);
+ }
+ } catch (SubtitleDecoderException e) {
+ Log.w(TAG, "Suppressing parser error", e);
+ // Treat the node (and by extension, all of its children) as unsupported.
+ unsupportedNodeDepth++;
+ }
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
+ ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap);
+ }
+ nodeStack.removeLast();
+ }
+ } else {
+ if (eventType == XmlPullParser.START_TAG) {
+ unsupportedNodeDepth++;
+ } else if (eventType == XmlPullParser.END_TAG) {
+ unsupportedNodeDepth--;
+ }
+ }
+ xmlParser.next();
+ eventType = xmlParser.getEventType();
+ }
+ return ttmlSubtitle;
+ } catch (XmlPullParserException xppe) {
+ throw new SubtitleDecoderException("Unable to decode source", xppe);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unexpected error when reading input.", e);
+ }
+ }
+
+ private FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)
+ throws SubtitleDecoderException {
+ int frameRate = DEFAULT_FRAME_RATE;
+ String frameRateString = xmlParser.getAttributeValue(TTP, "frameRate");
+ if (frameRateString != null) {
+ frameRate = Integer.parseInt(frameRateString);
+ }
+
+ float frameRateMultiplier = 1;
+ String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier");
+ if (frameRateMultiplierString != null) {
+ String[] parts = frameRateMultiplierString.split(" ");
+ if (parts.length != 2) {
+ throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts");
+ }
+ float numerator = Integer.parseInt(parts[0]);
+ float denominator = Integer.parseInt(parts[1]);
+ frameRateMultiplier = numerator / denominator;
+ }
+
+ int subFrameRate = DEFAULT_FRAME_AND_TICK_RATE.subFrameRate;
+ String subFrameRateString = xmlParser.getAttributeValue(TTP, "subFrameRate");
+ if (subFrameRateString != null) {
+ subFrameRate = Integer.parseInt(subFrameRateString);
+ }
+
+ int tickRate = DEFAULT_FRAME_AND_TICK_RATE.tickRate;
+ String tickRateString = xmlParser.getAttributeValue(TTP, "tickRate");
+ if (tickRateString != null) {
+ tickRate = Integer.parseInt(tickRateString);
+ }
+ return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
+ }
+
+ private Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser,
+ Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions)
+ throws IOException, XmlPullParserException {
+ do {
+ xmlParser.next();
+ if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) {
+ String parentStyleId = XmlPullParserUtil.getAttributeValue(xmlParser, ATTR_STYLE);
+ TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());
+ if (parentStyleId != null) {
+ for (String id : parseStyleIds(parentStyleId)) {
+ style.chain(globalStyles.get(id));
+ }
+ }
+ if (style.getId() != null) {
+ globalStyles.put(style.getId(), style);
+ }
+ } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
+ Pair<String, TtmlRegion> ttmlRegionInfo = parseRegionAttributes(xmlParser);
+ if (ttmlRegionInfo != null) {
+ globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second);
+ }
+ }
+ } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
+ return globalStyles;
+ }
+
+ /**
+ * Parses a region declaration. Supports origin and extent definition but only when defined in
+ * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored.
+ */
+ private Pair<String, TtmlRegion> parseRegionAttributes(XmlPullParser xmlParser) {
+ String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
+ String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
+ String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
+ if (regionOrigin == null || regionId == null) {
+ return null;
+ }
+ float position = Cue.DIMEN_UNSET;
+ float line = Cue.DIMEN_UNSET;
+ Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
+ if (originMatcher.matches()) {
+ try {
+ position = Float.parseFloat(originMatcher.group(1)) / 100.f;
+ line = Float.parseFloat(originMatcher.group(2)) / 100.f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e);
+ position = Cue.DIMEN_UNSET;
+ }
+ }
+ float width = Cue.DIMEN_UNSET;
+ if (regionExtent != null) {
+ Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
+ if (extentMatcher.matches()) {
+ try {
+ width = Float.parseFloat(extentMatcher.group(1)) / 100.f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e);
+ }
+ }
+ }
+ return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line,
+ Cue.LINE_TYPE_FRACTION, width)) : null;
+ }
+
+ private String[] parseStyleIds(String parentStyleIds) {
+ return parentStyleIds.split("\\s+");
+ }
+
+ private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attributeValue = parser.getAttributeValue(i);
+ switch (parser.getAttributeName(i)) {
+ case TtmlNode.ATTR_ID:
+ if (TtmlNode.TAG_STYLE.equals(parser.getName())) {
+ style = createIfNull(style).setId(attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_BACKGROUND_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "failed parsing background value: '" + attributeValue + "'");
+ }
+ break;
+ case TtmlNode.ATTR_TTS_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "failed parsing color value: '" + attributeValue + "'");
+ }
+ break;
+ case TtmlNode.ATTR_TTS_FONT_FAMILY:
+ style = createIfNull(style).setFontFamily(attributeValue);
+ break;
+ case TtmlNode.ATTR_TTS_FONT_SIZE:
+ try {
+ style = createIfNull(style);
+ parseFontSize(attributeValue, style);
+ } catch (SubtitleDecoderException e) {
+ Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'");
+ }
+ break;
+ case TtmlNode.ATTR_TTS_FONT_WEIGHT:
+ style = createIfNull(style).setBold(
+ TtmlNode.BOLD.equalsIgnoreCase(attributeValue));
+ break;
+ case TtmlNode.ATTR_TTS_FONT_STYLE:
+ style = createIfNull(style).setItalic(
+ TtmlNode.ITALIC.equalsIgnoreCase(attributeValue));
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_ALIGN:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.LEFT:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);
+ break;
+ case TtmlNode.START:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);
+ break;
+ case TtmlNode.RIGHT:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);
+ break;
+ case TtmlNode.END:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);
+ break;
+ case TtmlNode.CENTER:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER);
+ break;
+ }
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_DECORATION:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.LINETHROUGH:
+ style = createIfNull(style).setLinethrough(true);
+ break;
+ case TtmlNode.NO_LINETHROUGH:
+ style = createIfNull(style).setLinethrough(false);
+ break;
+ case TtmlNode.UNDERLINE:
+ style = createIfNull(style).setUnderline(true);
+ break;
+ case TtmlNode.NO_UNDERLINE:
+ style = createIfNull(style).setUnderline(false);
+ break;
+ }
+ break;
+ default:
+ // ignore
+ break;
+ }
+ }
+ return style;
+ }
+
+ private TtmlStyle createIfNull(TtmlStyle style) {
+ return style == null ? new TtmlStyle() : style;
+ }
+
+ private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent,
+ Map<String, TtmlRegion> regionMap, FrameAndTickRate frameAndTickRate)
+ throws SubtitleDecoderException {
+ long duration = C.TIME_UNSET;
+ long startTime = C.TIME_UNSET;
+ long endTime = C.TIME_UNSET;
+ String regionId = TtmlNode.ANONYMOUS_REGION_ID;
+ String[] styleIds = null;
+ int attributeCount = parser.getAttributeCount();
+ TtmlStyle style = parseStyleAttributes(parser, null);
+ for (int i = 0; i < attributeCount; i++) {
+ String attr = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ switch (attr) {
+ case ATTR_BEGIN:
+ startTime = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_END:
+ endTime = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_DURATION:
+ duration = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_STYLE:
+ // IDREFS: potentially multiple space delimited ids
+ String[] ids = parseStyleIds(value);
+ if (ids.length > 0) {
+ styleIds = ids;
+ }
+ break;
+ case ATTR_REGION:
+ if (regionMap.containsKey(value)) {
+ // If the region has not been correctly declared or does not define a position, we use
+ // the anonymous region.
+ regionId = value;
+ }
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+ if (parent != null && parent.startTimeUs != C.TIME_UNSET) {
+ if (startTime != C.TIME_UNSET) {
+ startTime += parent.startTimeUs;
+ }
+ if (endTime != C.TIME_UNSET) {
+ endTime += parent.startTimeUs;
+ }
+ }
+ if (endTime == C.TIME_UNSET) {
+ if (duration != C.TIME_UNSET) {
+ // Infer the end time from the duration.
+ endTime = startTime + duration;
+ } else if (parent != null && parent.endTimeUs != C.TIME_UNSET) {
+ // If the end time remains unspecified, then it should be inherited from the parent.
+ endTime = parent.endTimeUs;
+ }
+ }
+ return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId);
+ }
+
+ private static boolean isSupportedTag(String tag) {
+ return tag.equals(TtmlNode.TAG_TT)
+ || tag.equals(TtmlNode.TAG_HEAD)
+ || tag.equals(TtmlNode.TAG_BODY)
+ || tag.equals(TtmlNode.TAG_DIV)
+ || tag.equals(TtmlNode.TAG_P)
+ || tag.equals(TtmlNode.TAG_SPAN)
+ || tag.equals(TtmlNode.TAG_BR)
+ || tag.equals(TtmlNode.TAG_STYLE)
+ || tag.equals(TtmlNode.TAG_STYLING)
+ || tag.equals(TtmlNode.TAG_LAYOUT)
+ || tag.equals(TtmlNode.TAG_REGION)
+ || tag.equals(TtmlNode.TAG_METADATA)
+ || tag.equals(TtmlNode.TAG_SMPTE_IMAGE)
+ || tag.equals(TtmlNode.TAG_SMPTE_DATA)
+ || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION);
+ }
+
+ private static void parseFontSize(String expression, TtmlStyle out) throws
+ SubtitleDecoderException {
+ String[] expressions = expression.split("\\s+");
+ Matcher matcher;
+ if (expressions.length == 1) {
+ matcher = FONT_SIZE.matcher(expression);
+ } else if (expressions.length == 2){
+ matcher = FONT_SIZE.matcher(expressions[1]);
+ Log.w(TAG, "Multiple values in fontSize attribute. Picking the second value for vertical font"
+ + " size and ignoring the first.");
+ } else {
+ throw new SubtitleDecoderException("Invalid number of entries for fontSize: "
+ + expressions.length + ".");
+ }
+
+ if (matcher.matches()) {
+ String unit = matcher.group(3);
+ switch (unit) {
+ case "px":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PIXEL);
+ break;
+ case "em":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_EM);
+ break;
+ case "%":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PERCENT);
+ break;
+ default:
+ throw new SubtitleDecoderException("Invalid unit for fontSize: '" + unit + "'.");
+ }
+ out.setFontSize(Float.valueOf(matcher.group(1)));
+ } else {
+ throw new SubtitleDecoderException("Invalid expression for fontSize: '" + expression + "'.");
+ }
+ }
+
+ /**
+ * Parses a time expression, returning the parsed timestamp.
+ * <p>
+ * For the format of a time expression, see:
+ * <a href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
+ *
+ * @param time A string that includes the time expression.
+ * @param frameAndTickRate The effective frame and tick rates of the stream.
+ * @return The parsed timestamp in microseconds.
+ * @throws SubtitleDecoderException If the given string does not contain a valid time expression.
+ */
+ private static long parseTimeExpression(String time, FrameAndTickRate frameAndTickRate)
+ throws SubtitleDecoderException {
+ Matcher matcher = CLOCK_TIME.matcher(time);
+ if (matcher.matches()) {
+ String hours = matcher.group(1);
+ double durationSeconds = Long.parseLong(hours) * 3600;
+ String minutes = matcher.group(2);
+ durationSeconds += Long.parseLong(minutes) * 60;
+ String seconds = matcher.group(3);
+ durationSeconds += Long.parseLong(seconds);
+ String fraction = matcher.group(4);
+ durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
+ String frames = matcher.group(5);
+ durationSeconds += (frames != null)
+ ? Long.parseLong(frames) / frameAndTickRate.effectiveFrameRate : 0;
+ String subframes = matcher.group(6);
+ durationSeconds += (subframes != null)
+ ? ((double) Long.parseLong(subframes)) / frameAndTickRate.subFrameRate
+ / frameAndTickRate.effectiveFrameRate
+ : 0;
+ return (long) (durationSeconds * C.MICROS_PER_SECOND);
+ }
+ matcher = OFFSET_TIME.matcher(time);
+ if (matcher.matches()) {
+ String timeValue = matcher.group(1);
+ double offsetSeconds = Double.parseDouble(timeValue);
+ String unit = matcher.group(2);
+ switch (unit) {
+ case "h":
+ offsetSeconds *= 3600;
+ break;
+ case "m":
+ offsetSeconds *= 60;
+ break;
+ case "s":
+ // Do nothing.
+ break;
+ case "ms":
+ offsetSeconds /= 1000;
+ break;
+ case "f":
+ offsetSeconds /= frameAndTickRate.effectiveFrameRate;
+ break;
+ case "t":
+ offsetSeconds /= frameAndTickRate.tickRate;
+ break;
+ }
+ return (long) (offsetSeconds * C.MICROS_PER_SECOND);
+ }
+ throw new SubtitleDecoderException("Malformed time expression: " + time);
+ }
+
+ private static final class FrameAndTickRate {
+ final float effectiveFrameRate;
+ final int subFrameRate;
+ final int tickRate;
+
+ FrameAndTickRate(float effectiveFrameRate, int subFrameRate, int tickRate) {
+ this.effectiveFrameRate = effectiveFrameRate;
+ this.subFrameRate = subFrameRate;
+ this.tickRate = tickRate;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -0,0 +1,296 @@
+/*
+ * 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 com.google.android.exoplayer2.text.ttml;
+
+import android.text.SpannableStringBuilder;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * A package internal representation of TTML node.
+ */
+/* package */ final class TtmlNode {
+
+ public static final String TAG_TT = "tt";
+ public static final String TAG_HEAD = "head";
+ public static final String TAG_BODY = "body";
+ public static final String TAG_DIV = "div";
+ public static final String TAG_P = "p";
+ public static final String TAG_SPAN = "span";
+ public static final String TAG_BR = "br";
+ public static final String TAG_STYLE = "style";
+ public static final String TAG_STYLING = "styling";
+ public static final String TAG_LAYOUT = "layout";
+ public static final String TAG_REGION = "region";
+ public static final String TAG_METADATA = "metadata";
+ public static final String TAG_SMPTE_IMAGE = "smpte:image";
+ public static final String TAG_SMPTE_DATA = "smpte:data";
+ public static final String TAG_SMPTE_INFORMATION = "smpte:information";
+
+ public static final String ANONYMOUS_REGION_ID = "";
+ public static final String ATTR_ID = "id";
+ public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
+ public static final String ATTR_TTS_EXTENT = "extent";
+ public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
+ public static final String ATTR_TTS_FONT_SIZE = "fontSize";
+ public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
+ public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
+ public static final String ATTR_TTS_COLOR = "color";
+ public static final String ATTR_TTS_ORIGIN = "origin";
+ public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
+ public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
+
+ public static final String LINETHROUGH = "linethrough";
+ public static final String NO_LINETHROUGH = "nolinethrough";
+ public static final String UNDERLINE = "underline";
+ public static final String NO_UNDERLINE = "nounderline";
+ public static final String ITALIC = "italic";
+ public static final String BOLD = "bold";
+
+ public static final String LEFT = "left";
+ public static final String CENTER = "center";
+ public static final String RIGHT = "right";
+ public static final String START = "start";
+ public static final String END = "end";
+
+ public final String tag;
+ public final String text;
+ public final boolean isTextNode;
+ public final long startTimeUs;
+ public final long endTimeUs;
+ public final TtmlStyle style;
+ public final String regionId;
+
+ private final String[] styleIds;
+ private final HashMap<String, Integer> nodeStartsByRegion;
+ private final HashMap<String, Integer> nodeEndsByRegion;
+
+ private List<TtmlNode> children;
+
+ public static TtmlNode buildTextNode(String text) {
+ return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET,
+ C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID);
+ }
+
+ public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
+ TtmlStyle style, String[] styleIds, String regionId) {
+ return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId);
+ }
+
+ private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
+ TtmlStyle style, String[] styleIds, String regionId) {
+ this.tag = tag;
+ this.text = text;
+ this.style = style;
+ this.styleIds = styleIds;
+ this.isTextNode = text != null;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ this.regionId = Assertions.checkNotNull(regionId);
+ nodeStartsByRegion = new HashMap<>();
+ nodeEndsByRegion = new HashMap<>();
+ }
+
+ public boolean isActive(long timeUs) {
+ return (startTimeUs == C.TIME_UNSET && endTimeUs == C.TIME_UNSET)
+ || (startTimeUs <= timeUs && endTimeUs == C.TIME_UNSET)
+ || (startTimeUs == C.TIME_UNSET && timeUs < endTimeUs)
+ || (startTimeUs <= timeUs && timeUs < endTimeUs);
+ }
+
+ public void addChild(TtmlNode child) {
+ if (children == null) {
+ children = new ArrayList<>();
+ }
+ children.add(child);
+ }
+
+ public TtmlNode getChild(int index) {
+ if (children == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return children.get(index);
+ }
+
+ public int getChildCount() {
+ return children == null ? 0 : children.size();
+ }
+
+ public long[] getEventTimesUs() {
+ TreeSet<Long> eventTimeSet = new TreeSet<>();
+ getEventTimes(eventTimeSet, false);
+ long[] eventTimes = new long[eventTimeSet.size()];
+ int i = 0;
+ for (long eventTimeUs : eventTimeSet) {
+ eventTimes[i++] = eventTimeUs;
+ }
+ return eventTimes;
+ }
+
+ private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
+ boolean isPNode = TAG_P.equals(tag);
+ if (descendsPNode || isPNode) {
+ if (startTimeUs != C.TIME_UNSET) {
+ out.add(startTimeUs);
+ }
+ if (endTimeUs != C.TIME_UNSET) {
+ out.add(endTimeUs);
+ }
+ }
+ if (children == null) {
+ return;
+ }
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).getEventTimes(out, descendsPNode || isPNode);
+ }
+ }
+
+ public String[] getStyleIds() {
+ return styleIds;
+ }
+
+ public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles,
+ Map<String, TtmlRegion> regionMap) {
+ TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
+ traverseForText(timeUs, false, regionId, regionOutputs);
+ traverseForStyle(globalStyles, regionOutputs);
+ List<Cue> cues = new ArrayList<>();
+ for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+ TtmlRegion region = regionMap.get(entry.getKey());
+ cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
+ Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width));
+ }
+ return cues;
+ }
+
+ private void traverseForText(long timeUs, boolean descendsPNode,
+ String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) {
+ nodeStartsByRegion.clear();
+ nodeEndsByRegion.clear();
+ String resolvedRegionId = regionId;
+ if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) {
+ resolvedRegionId = inheritedRegion;
+ }
+ if (isTextNode && descendsPNode) {
+ getRegionOutput(resolvedRegionId, regionOutputs).append(text);
+ } else if (TAG_BR.equals(tag) && descendsPNode) {
+ getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
+ } else if (TAG_METADATA.equals(tag)) {
+ // Do nothing.
+ } else if (isActive(timeUs)) {
+ boolean isPNode = TAG_P.equals(tag);
+ for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+ nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
+ }
+ for (int i = 0; i < getChildCount(); i++) {
+ getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
+ regionOutputs);
+ }
+ if (isPNode) {
+ TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
+ }
+ for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+ nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
+ }
+ }
+ }
+
+ private static SpannableStringBuilder getRegionOutput(String resolvedRegionId,
+ Map<String, SpannableStringBuilder> regionOutputs) {
+ if (!regionOutputs.containsKey(resolvedRegionId)) {
+ regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
+ }
+ return regionOutputs.get(resolvedRegionId);
+ }
+
+ private void traverseForStyle(Map<String, TtmlStyle> globalStyles,
+ Map<String, SpannableStringBuilder> regionOutputs) {
+ for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
+ String regionId = entry.getKey();
+ int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
+ applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue());
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChild(i).traverseForStyle(globalStyles, regionOutputs);
+ }
+ }
+ }
+
+ private void applyStyleToOutput(Map<String, TtmlStyle> globalStyles,
+ SpannableStringBuilder regionOutput, int start, int end) {
+ if (start != end) {
+ TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
+ if (resolvedStyle != null) {
+ TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
+ }
+ }
+ }
+
+ private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
+ // Having joined the text elements, we need to do some final cleanup on the result.
+ // 1. Collapse multiple consecutive spaces into a single space.
+ int builderLength = builder.length();
+ for (int i = 0; i < builderLength; i++) {
+ if (builder.charAt(i) == ' ') {
+ int j = i + 1;
+ while (j < builder.length() && builder.charAt(j) == ' ') {
+ j++;
+ }
+ int spacesToDelete = j - (i + 1);
+ if (spacesToDelete > 0) {
+ builder.delete(i, i + spacesToDelete);
+ builderLength -= spacesToDelete;
+ }
+ }
+ }
+ // 2. Remove any spaces from the start of each line.
+ if (builderLength > 0 && builder.charAt(0) == ' ') {
+ builder.delete(0, 1);
+ builderLength--;
+ }
+ for (int i = 0; i < builderLength - 1; i++) {
+ if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') {
+ builder.delete(i + 1, i + 2);
+ builderLength--;
+ }
+ }
+ // 3. Remove any spaces from the end of each line.
+ if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') {
+ builder.delete(builderLength - 1, builderLength);
+ builderLength--;
+ }
+ for (int i = 0; i < builderLength - 1; i++) {
+ if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') {
+ builder.delete(i, i + 1);
+ builderLength--;
+ }
+ }
+ // 4. Trim a trailing newline, if there is one.
+ if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') {
+ builder.delete(builderLength - 1, builderLength);
+ /*builderLength--;*/
+ }
+ return builder;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.google.android.exoplayer2.text.ttml;
+
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * Represents a TTML Region.
+ */
+/* package */ final class TtmlRegion {
+
+ public final float position;
+ public final float line;
+ @Cue.LineType
+ public final int lineType;
+ public final float width;
+
+ public TtmlRegion() {
+ this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
+ }
+
+ public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) {
+ this.position = position;
+ this.line = line;
+ this.lineType = lineType;
+ this.width = width;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
@@ -0,0 +1,151 @@
+/*
+ * 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 com.google.android.exoplayer2.text.ttml;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+import java.util.Map;
+
+/**
+ * Package internal utility class to render styled <code>TtmlNode</code>s.
+ */
+/* package */ final class TtmlRenderUtil {
+
+ public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds,
+ Map<String, TtmlStyle> globalStyles) {
+ if (style == null && styleIds == null) {
+ // No styles at all.
+ return null;
+ } else if (style == null && styleIds.length == 1) {
+ // Only one single referential style present.
+ return globalStyles.get(styleIds[0]);
+ } else if (style == null && styleIds.length > 1) {
+ // Only multiple referential styles present.
+ TtmlStyle chainedStyle = new TtmlStyle();
+ for (String id : styleIds) {
+ chainedStyle.chain(globalStyles.get(id));
+ }
+ return chainedStyle;
+ } else if (style != null && styleIds != null && styleIds.length == 1) {
+ // Merge a single referential style into inline style.
+ return style.chain(globalStyles.get(styleIds[0]));
+ } else if (style != null && styleIds != null && styleIds.length > 1) {
+ // Merge multiple referential styles into inline style.
+ for (String id : styleIds) {
+ style.chain(globalStyles.get(id));
+ }
+ return style;
+ }
+ // Only inline styles available.
+ return style;
+ }
+
+ public static void applyStylesToSpan(SpannableStringBuilder builder,
+ int start, int end, TtmlStyle style) {
+
+ if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
+ builder.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getTextAlign() != null) {
+ builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ switch (style.getFontSizeUnit()) {
+ case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
+ builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.FONT_SIZE_UNIT_EM:
+ builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
+ builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.UNSPECIFIED:
+ // Do nothing.
+ break;
+ }
+ }
+
+ /**
+ * Called when the end of a paragraph is encountered. Adds a newline if there are one or more
+ * non-space characters since the previous newline.
+ *
+ * @param builder The builder.
+ */
+ /* package */ static void endParagraph(SpannableStringBuilder builder) {
+ int position = builder.length() - 1;
+ while (position >= 0 && builder.charAt(position) == ' ') {
+ position--;
+ }
+ if (position >= 0 && builder.charAt(position) != '\n') {
+ builder.append('\n');
+ }
+ }
+
+ /**
+ * Applies the appropriate space policy to the given text element.
+ *
+ * @param in The text element to which the policy should be applied.
+ * @return The result of applying the policy to the text element.
+ */
+ /* package */ static String applyTextElementSpacePolicy(String in) {
+ // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
+ String out = in.replaceAll("\r\n", "\n");
+ // Apply suppress-at-line-break="auto" and
+ // white-space-treatment="ignore-if-surrounding-linefeed"
+ out = out.replaceAll(" *\n *", "\n");
+ // Apply linefeed-treatment="treat-as-space"
+ out = out.replaceAll("\n", " ");
+ // Apply white-space-collapse="true"
+ out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
+ return out;
+ }
+
+ private TtmlRenderUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
@@ -0,0 +1,260 @@
+/*
+ * 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 com.google.android.exoplayer2.text.ttml;
+
+import android.graphics.Typeface;
+import android.support.annotation.IntDef;
+import android.text.Layout;
+import com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Style object of a <code>TtmlNode</code>
+ */
+/* package */ final class TtmlStyle {
+
+ public static final int UNSPECIFIED = -1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC,
+ STYLE_BOLD_ITALIC})
+ public @interface StyleFlags {}
+ public static final int STYLE_NORMAL = Typeface.NORMAL;
+ public static final int STYLE_BOLD = Typeface.BOLD;
+ public static final int STYLE_ITALIC = Typeface.ITALIC;
+ public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})
+ public @interface FontSizeUnit {}
+ public static final int FONT_SIZE_UNIT_PIXEL = 1;
+ public static final int FONT_SIZE_UNIT_EM = 2;
+ public static final int FONT_SIZE_UNIT_PERCENT = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, OFF, ON})
+ private @interface OptionalBoolean {}
+ private static final int OFF = 0;
+ private static final int ON = 1;
+
+ private String fontFamily;
+ private int fontColor;
+ private boolean hasFontColor;
+ private int backgroundColor;
+ private boolean hasBackgroundColor;
+ @OptionalBoolean private int linethrough;
+ @OptionalBoolean private int underline;
+ @OptionalBoolean private int bold;
+ @OptionalBoolean private int italic;
+ @FontSizeUnit private int fontSizeUnit;
+ private float fontSize;
+ private String id;
+ private TtmlStyle inheritableStyle;
+ private Layout.Alignment textAlign;
+
+ public TtmlStyle() {
+ linethrough = UNSPECIFIED;
+ underline = UNSPECIFIED;
+ bold = UNSPECIFIED;
+ italic = UNSPECIFIED;
+ fontSizeUnit = UNSPECIFIED;
+ }
+
+ /**
+ * Returns the style or {@link #UNSPECIFIED} when no style information is given.
+ *
+ * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
+ * or {@link #STYLE_BOLD_ITALIC}.
+ */
+ @StyleFlags public int getStyle() {
+ if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
+ return UNSPECIFIED;
+ }
+ return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)
+ | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);
+ }
+
+ public boolean isLinethrough() {
+ return linethrough == ON;
+ }
+
+ public TtmlStyle setLinethrough(boolean linethrough) {
+ Assertions.checkState(inheritableStyle == null);
+ this.linethrough = linethrough ? ON : OFF;
+ return this;
+ }
+
+ public boolean isUnderline() {
+ return underline == ON;
+ }
+
+ public TtmlStyle setUnderline(boolean underline) {
+ Assertions.checkState(inheritableStyle == null);
+ this.underline = underline ? ON : OFF;
+ return this;
+ }
+
+ public TtmlStyle setBold(boolean bold) {
+ Assertions.checkState(inheritableStyle == null);
+ this.bold = bold ? ON : OFF;
+ return this;
+ }
+
+ public TtmlStyle setItalic(boolean italic) {
+ Assertions.checkState(inheritableStyle == null);
+ this.italic = italic ? ON : OFF;
+ return this;
+ }
+
+ public String getFontFamily() {
+ return fontFamily;
+ }
+
+ public TtmlStyle setFontFamily(String fontFamily) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontFamily = fontFamily;
+ return this;
+ }
+
+ public int getFontColor() {
+ if (!hasFontColor) {
+ throw new IllegalStateException("Font color has not been defined.");
+ }
+ return fontColor;
+ }
+
+ public TtmlStyle setFontColor(int fontColor) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontColor = fontColor;
+ hasFontColor = true;
+ return this;
+ }
+
+ public boolean hasFontColor() {
+ return hasFontColor;
+ }
+
+ public int getBackgroundColor() {
+ if (!hasBackgroundColor) {
+ throw new IllegalStateException("Background color has not been defined.");
+ }
+ return backgroundColor;
+ }
+
+ public TtmlStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ hasBackgroundColor = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColor() {
+ return hasBackgroundColor;
+ }
+
+ /**
+ * Inherits from an ancestor style. Properties like <i>tts:backgroundColor</i> which
+ * are not inheritable are not inherited as well as properties which are already set locally
+ * are never overridden.
+ *
+ * @param ancestor the ancestor style to inherit from
+ */
+ public TtmlStyle inherit(TtmlStyle ancestor) {
+ return inherit(ancestor, false);
+ }
+
+ /**
+ * Chains this style to referential style. Local properties which are already set
+ * are never overridden.
+ *
+ * @param ancestor the referential style to inherit from
+ */
+ public TtmlStyle chain(TtmlStyle ancestor) {
+ return inherit(ancestor, true);
+ }
+
+ private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) {
+ if (ancestor != null) {
+ if (!hasFontColor && ancestor.hasFontColor) {
+ setFontColor(ancestor.fontColor);
+ }
+ if (bold == UNSPECIFIED) {
+ bold = ancestor.bold;
+ }
+ if (italic == UNSPECIFIED) {
+ italic = ancestor.italic;
+ }
+ if (fontFamily == null) {
+ fontFamily = ancestor.fontFamily;
+ }
+ if (linethrough == UNSPECIFIED) {
+ linethrough = ancestor.linethrough;
+ }
+ if (underline == UNSPECIFIED) {
+ underline = ancestor.underline;
+ }
+ if (textAlign == null) {
+ textAlign = ancestor.textAlign;
+ }
+ if (fontSizeUnit == UNSPECIFIED) {
+ fontSizeUnit = ancestor.fontSizeUnit;
+ fontSize = ancestor.fontSize;
+ }
+ // attributes not inherited as of http://www.w3.org/TR/ttml1/
+ if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
+ setBackgroundColor(ancestor.backgroundColor);
+ }
+ }
+ return this;
+ }
+
+ public TtmlStyle setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Layout.Alignment getTextAlign() {
+ return textAlign;
+ }
+
+ public TtmlStyle setTextAlign(Layout.Alignment textAlign) {
+ this.textAlign = textAlign;
+ return this;
+ }
+
+ public TtmlStyle setFontSize(float fontSize) {
+ this.fontSize = fontSize;
+ return this;
+ }
+
+ public TtmlStyle setFontSizeUnit(int fontSizeUnit) {
+ this.fontSizeUnit = fontSizeUnit;
+ return this;
+ }
+
+ @FontSizeUnit public int getFontSizeUnit() {
+ return fontSizeUnit;
+ }
+
+ public float getFontSize() {
+ return fontSize;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.google.android.exoplayer2.text.ttml;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A representation of a TTML subtitle.
+ */
+/* package */ final class TtmlSubtitle implements Subtitle {
+
+ private final TtmlNode root;
+ private final long[] eventTimesUs;
+ private final Map<String, TtmlStyle> globalStyles;
+ private final Map<String, TtmlRegion> regionMap;
+
+ public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles,
+ Map<String, TtmlRegion> regionMap) {
+ this.root = root;
+ this.regionMap = regionMap;
+ this.globalStyles = globalStyles != null
+ ? Collections.unmodifiableMap(globalStyles) : Collections.<String, TtmlStyle>emptyMap();
+ this.eventTimesUs = root.getEventTimesUs();
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(eventTimesUs, timeUs, false, false);
+ return index < eventTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return eventTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return eventTimesUs[index];
+ }
+
+ /* @VisibleForTesting */
+ /* package */ TtmlNode getRoot() {
+ return root;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return root.getCues(timeUs, globalStyles, regionMap);
+ }
+
+ /* @VisibleForTesting */
+ /* package */ Map<String, TtmlStyle> getGlobalStyles() {
+ return globalStyles;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java
@@ -0,0 +1,234 @@
+/*
+ * 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 com.google.android.exoplayer2.text.tx3g;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.charset.Charset;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for tx3g.
+ * <p>
+ * Currently supports parsing of a single text track with embedded styles.
+ */
+public final class Tx3gDecoder extends SimpleSubtitleDecoder {
+
+ private static final char BOM_UTF16_BE = '\uFEFF';
+ private static final char BOM_UTF16_LE = '\uFFFE';
+
+ private static final int TYPE_STYL = Util.getIntegerCodeForString("styl");
+ private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox");
+ private static final String TX3G_SERIF = "Serif";
+
+ private static final int SIZE_ATOM_HEADER = 8;
+ private static final int SIZE_SHORT = 2;
+ private static final int SIZE_BOM_UTF16 = 2;
+ private static final int SIZE_STYLE_RECORD = 12;
+
+ private static final int FONT_FACE_BOLD = 0x0001;
+ private static final int FONT_FACE_ITALIC = 0x0002;
+ private static final int FONT_FACE_UNDERLINE = 0x0004;
+
+ private static final int SPAN_PRIORITY_LOW = (0xFF << Spanned.SPAN_PRIORITY_SHIFT);
+ private static final int SPAN_PRIORITY_HIGH = (0x00 << Spanned.SPAN_PRIORITY_SHIFT);
+
+ private static final int DEFAULT_FONT_FACE = 0;
+ private static final int DEFAULT_COLOR = Color.WHITE;
+ private static final String DEFAULT_FONT_FAMILY = C.SANS_SERIF_NAME;
+ private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;
+
+ private final ParsableByteArray parsableByteArray;
+ private boolean customVerticalPlacement;
+ private int defaultFontFace;
+ private int defaultColorRgba;
+ private String defaultFontFamily;
+ private float defaultVerticalPlacement;
+ private int calculatedVideoTrackHeight;
+
+ /**
+ * Sets up a new {@link Tx3gDecoder} with default values.
+ *
+ * @param initializationData Sample description atom ('stsd') data with default subtitle styles.
+ */
+ public Tx3gDecoder(List<byte[]> initializationData) {
+ super("Tx3gDecoder");
+ parsableByteArray = new ParsableByteArray();
+ decodeInitializationData(initializationData);
+ }
+
+ private void decodeInitializationData(List<byte[]> initializationData) {
+ if (initializationData != null && initializationData.size() == 1
+ && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {
+ byte[] initializationBytes = initializationData.get(0);
+ defaultFontFace = initializationBytes[24];
+ defaultColorRgba = ((initializationBytes[26] & 0xFF) << 24)
+ | ((initializationBytes[27] & 0xFF) << 16)
+ | ((initializationBytes[28] & 0xFF) << 8)
+ | (initializationBytes[29] & 0xFF);
+ String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43);
+ defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME;
+ //font size (initializationBytes[25]) is 5% of video height
+ calculatedVideoTrackHeight = 20 * initializationBytes[25];
+ customVerticalPlacement = (initializationBytes[0] & 0x20) != 0;
+ if (customVerticalPlacement) {
+ int requestedVerticalPlacement = ((initializationBytes[10] & 0xFF) << 8)
+ | (initializationBytes[11] & 0xFF);
+ defaultVerticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;
+ defaultVerticalPlacement = Util.constrainValue(defaultVerticalPlacement, 0.0f, 0.95f);
+ } else {
+ defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;
+ }
+ } else {
+ defaultFontFace = DEFAULT_FONT_FACE;
+ defaultColorRgba = DEFAULT_COLOR;
+ defaultFontFamily = DEFAULT_FONT_FAMILY;
+ customVerticalPlacement = false;
+ defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;
+ }
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ parsableByteArray.reset(bytes, length);
+ String cueTextString = readSubtitleText(parsableByteArray);
+ if (cueTextString.isEmpty()) {
+ return Tx3gSubtitle.EMPTY;
+ }
+ // Attach default styles.
+ SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString);
+ attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(),
+ SPAN_PRIORITY_LOW);
+ attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(),
+ SPAN_PRIORITY_LOW);
+ attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(),
+ SPAN_PRIORITY_LOW);
+ float verticalPlacement = defaultVerticalPlacement;
+ // Find and attach additional styles.
+ while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) {
+ int position = parsableByteArray.getPosition();
+ int atomSize = parsableByteArray.readInt();
+ int atomType = parsableByteArray.readInt();
+ if (atomType == TYPE_STYL) {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
+ int styleRecordCount = parsableByteArray.readUnsignedShort();
+ for (int i = 0; i < styleRecordCount; i++) {
+ applyStyleRecord(parsableByteArray, cueText);
+ }
+ } else if (atomType == TYPE_TBOX && customVerticalPlacement) {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
+ int requestedVerticalPlacement = parsableByteArray.readUnsignedShort();
+ verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;
+ verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f);
+ }
+ parsableByteArray.setPosition(position + atomSize);
+ }
+ return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION,
+ Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET));
+ }
+
+ private static String readSubtitleText(ParsableByteArray parsableByteArray)
+ throws SubtitleDecoderException {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
+ int textLength = parsableByteArray.readUnsignedShort();
+ if (textLength == 0) {
+ return "";
+ }
+ if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) {
+ char firstChar = parsableByteArray.peekChar();
+ if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) {
+ return parsableByteArray.readString(textLength, Charset.forName(C.UTF16_NAME));
+ }
+ }
+ return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME));
+ }
+
+ private void applyStyleRecord(ParsableByteArray parsableByteArray,
+ SpannableStringBuilder cueText) throws SubtitleDecoderException {
+ assertTrue(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD);
+ int start = parsableByteArray.readUnsignedShort();
+ int end = parsableByteArray.readUnsignedShort();
+ parsableByteArray.skipBytes(2); // font identifier
+ int fontFace = parsableByteArray.readUnsignedByte();
+ parsableByteArray.skipBytes(1); // font size
+ int colorRgba = parsableByteArray.readInt();
+ attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);
+ attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
+ }
+
+ private static void attachFontFace(SpannableStringBuilder cueText, int fontFace,
+ int defaultFontFace, int start, int end, int spanPriority) {
+ if (fontFace != defaultFontFace) {
+ final int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority;
+ boolean isBold = (fontFace & FONT_FACE_BOLD) != 0;
+ boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0;
+ if (isBold) {
+ if (isItalic) {
+ cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flags);
+ } else {
+ cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, flags);
+ }
+ } else if (isItalic) {
+ cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flags);
+ }
+ boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0;
+ if (isUnderlined) {
+ cueText.setSpan(new UnderlineSpan(), start, end, flags);
+ }
+ if (!isUnderlined && !isBold && !isItalic) {
+ cueText.setSpan(new StyleSpan(Typeface.NORMAL), start, end, flags);
+ }
+ }
+ }
+
+ private static void attachColor(SpannableStringBuilder cueText, int colorRgba,
+ int defaultColorRgba, int start, int end, int spanPriority) {
+ if (colorRgba != defaultColorRgba) {
+ int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8);
+ cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);
+ }
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private static void attachFontFamily(SpannableStringBuilder cueText, String fontFamily,
+ String defaultFontFamily, int start, int end, int spanPriority) {
+ if (fontFamily != defaultFontFamily) {
+ cueText.setSpan(new TypefaceSpan(fontFamily), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);
+ }
+ }
+
+ private static void assertTrue(boolean checkValue) throws SubtitleDecoderException {
+ if (!checkValue) {
+ throw new SubtitleDecoderException("Unexpected subtitle format.");
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.google.android.exoplayer2.text.tx3g;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a tx3g subtitle.
+ */
+/* package */ final class Tx3gSubtitle implements Subtitle {
+
+ public static final Tx3gSubtitle EMPTY = new Tx3gSubtitle();
+
+ private final List<Cue> cues;
+
+ public Tx3gSubtitle(Cue cue) {
+ this.cues = Collections.singletonList(cue);
+ }
+
+ private Tx3gSubtitle() {
+ this.cues = Collections.emptyList();
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.<Cue>emptyList();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/CssParser.java
@@ -0,0 +1,331 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.util.ColorParser;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS
+ * features.
+ */
+/* package */ final class CssParser {
+
+ private static final String PROPERTY_BGCOLOR = "background-color";
+ private static final String PROPERTY_FONT_FAMILY = "font-family";
+ private static final String PROPERTY_FONT_WEIGHT = "font-weight";
+ private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
+ private static final String VALUE_BOLD = "bold";
+ private static final String VALUE_UNDERLINE = "underline";
+ private static final String BLOCK_START = "{";
+ private static final String BLOCK_END = "}";
+ private static final String PROPERTY_FONT_STYLE = "font-style";
+ private static final String VALUE_ITALIC = "italic";
+
+ private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
+
+ // Temporary utility data structures.
+ private final ParsableByteArray styleInput;
+ private final StringBuilder stringBuilder;
+
+ public CssParser() {
+ styleInput = new ParsableByteArray();
+ stringBuilder = new StringBuilder();
+ }
+
+ /**
+ * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the
+ * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or
+ * {@code null} otherwise.
+ *
+ * @param input The input from which the style block should be read.
+ * @return A {@link WebvttCssStyle} that represents the parsed block.
+ */
+ public WebvttCssStyle parseBlock(ParsableByteArray input) {
+ stringBuilder.setLength(0);
+ int initialInputPosition = input.getPosition();
+ skipStyleBlock(input);
+ styleInput.reset(input.data, input.getPosition());
+ styleInput.setPosition(initialInputPosition);
+ String selector = parseSelector(styleInput, stringBuilder);
+ if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
+ return null;
+ }
+ WebvttCssStyle style = new WebvttCssStyle();
+ applySelectorToStyle(style, selector);
+ String token = null;
+ boolean blockEndFound = false;
+ while (!blockEndFound) {
+ int position = styleInput.getPosition();
+ token = parseNextToken(styleInput, stringBuilder);
+ blockEndFound = token == null || BLOCK_END.equals(token);
+ if (!blockEndFound) {
+ styleInput.setPosition(position);
+ parseStyleDeclaration(styleInput, style, stringBuilder);
+ }
+ }
+ return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
+ }
+
+ /**
+ * Returns a string containing the selector. The input is expected to have the form
+ * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
+ *
+ * @param input From which the selector is obtained.
+ * @return A string containing the target, empty string if the selector is universal
+ * (targets all cues) or null if an error was encountered.
+ */
+ private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ if (input.bytesLeft() < 5) {
+ return null;
+ }
+ String cueSelector = input.readString(5);
+ if (!"::cue".equals(cueSelector)) {
+ return null;
+ }
+ int position = input.getPosition();
+ String token = parseNextToken(input, stringBuilder);
+ if (token == null) {
+ return null;
+ }
+ if (BLOCK_START.equals(token)) {
+ input.setPosition(position);
+ return "";
+ }
+ String target = null;
+ if ("(".equals(token)) {
+ target = readCueTarget(input);
+ }
+ token = parseNextToken(input, stringBuilder);
+ if (!")".equals(token) || token == null) {
+ return null;
+ }
+ return target;
+ }
+
+ /**
+ * Reads the contents of ::cue() and returns it as a string.
+ */
+ private static String readCueTarget(ParsableByteArray input) {
+ int position = input.getPosition();
+ int limit = input.limit();
+ boolean cueTargetEndFound = false;
+ while (position < limit && !cueTargetEndFound) {
+ char c = (char) input.data[position++];
+ cueTargetEndFound = c == ')';
+ }
+ return input.readString(--position - input.getPosition()).trim();
+ // --offset to return ')' to the input.
+ }
+
+ private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style,
+ StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ String property = parseIdentifier(input, stringBuilder);
+ if ("".equals(property)) {
+ return;
+ }
+ if (!":".equals(parseNextToken(input, stringBuilder))) {
+ return;
+ }
+ skipWhitespaceAndComments(input);
+ String value = parsePropertyValue(input, stringBuilder);
+ if (value == null || "".equals(value)) {
+ return;
+ }
+ int position = input.getPosition();
+ String token = parseNextToken(input, stringBuilder);
+ if (";".equals(token)) {
+ // The style declaration is well formed.
+ } else if (BLOCK_END.equals(token)) {
+ // The style declaration is well formed and we can go on, but the closing bracket had to be
+ // fed back.
+ input.setPosition(position);
+ } else {
+ // The style declaration is not well formed.
+ return;
+ }
+ // At this point we have a presumably valid declaration, we need to parse it and fill the style.
+ if ("color".equals(property)) {
+ style.setFontColor(ColorParser.parseCssColor(value));
+ } else if (PROPERTY_BGCOLOR.equals(property)) {
+ style.setBackgroundColor(ColorParser.parseCssColor(value));
+ } else if (PROPERTY_TEXT_DECORATION.equals(property)) {
+ if (VALUE_UNDERLINE.equals(value)) {
+ style.setUnderline(true);
+ }
+ } else if (PROPERTY_FONT_FAMILY.equals(property)) {
+ style.setFontFamily(value);
+ } else if (PROPERTY_FONT_WEIGHT.equals(property)) {
+ if (VALUE_BOLD.equals(value)) {
+ style.setBold(true);
+ }
+ } else if (PROPERTY_FONT_STYLE.equals(property)) {
+ if (VALUE_ITALIC.equals(value)) {
+ style.setItalic(true);
+ }
+ }
+ // TODO: Fill remaining supported styles.
+ }
+
+ // Visible for testing.
+ /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) {
+ boolean skipping = true;
+ while (input.bytesLeft() > 0 && skipping) {
+ skipping = maybeSkipWhitespace(input) || maybeSkipComment(input);
+ }
+ }
+
+ // Visible for testing.
+ /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ if (input.bytesLeft() == 0) {
+ return null;
+ }
+ String identifier = parseIdentifier(input, stringBuilder);
+ if (!"".equals(identifier)) {
+ return identifier;
+ }
+ // We found a delimiter.
+ return "" + (char) input.readUnsignedByte();
+ }
+
+ private static boolean maybeSkipWhitespace(ParsableByteArray input) {
+ switch(peekCharAtPosition(input, input.getPosition())) {
+ case '\t':
+ case '\r':
+ case '\n':
+ case '\f':
+ case ' ':
+ input.skipBytes(1);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Visible for testing.
+ /* package */ static void skipStyleBlock(ParsableByteArray input) {
+ // The style block cannot contain empty lines, so we assume the input ends when a empty line
+ // is found.
+ String line;
+ do {
+ line = input.readLine();
+ } while (!TextUtils.isEmpty(line));
+ }
+
+ private static char peekCharAtPosition(ParsableByteArray input, int position) {
+ return (char) input.data[position];
+ }
+
+ private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) {
+ StringBuilder expressionBuilder = new StringBuilder();
+ String token;
+ int position;
+ boolean expressionEndFound = false;
+ // TODO: Add support for "Strings in quotes with spaces".
+ while (!expressionEndFound) {
+ position = input.getPosition();
+ token = parseNextToken(input, stringBuilder);
+ if (token == null) {
+ // Syntax error.
+ return null;
+ }
+ if (BLOCK_END.equals(token) || ";".equals(token)) {
+ input.setPosition(position);
+ expressionEndFound = true;
+ } else {
+ expressionBuilder.append(token);
+ }
+ }
+ return expressionBuilder.toString();
+ }
+
+ private static boolean maybeSkipComment(ParsableByteArray input) {
+ int position = input.getPosition();
+ int limit = input.limit();
+ byte[] data = input.data;
+ if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') {
+ while (position + 1 < limit) {
+ char skippedChar = (char) data[position++];
+ if (skippedChar == '*') {
+ if (((char) data[position]) == '/') {
+ position++;
+ limit = position;
+ }
+ }
+ }
+ input.skipBytes(limit - input.getPosition());
+ return true;
+ }
+ return false;
+ }
+
+ private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) {
+ stringBuilder.setLength(0);
+ int position = input.getPosition();
+ int limit = input.limit();
+ boolean identifierEndFound = false;
+ while (position < limit && !identifierEndFound) {
+ char c = (char) input.data[position];
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#'
+ || c == '-' || c == '.' || c == '_') {
+ position++;
+ stringBuilder.append(c);
+ } else {
+ identifierEndFound = true;
+ }
+ }
+ input.skipBytes(position - input.getPosition());
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form
+ * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
+ */
+ private void applySelectorToStyle(WebvttCssStyle style, String selector) {
+ if ("".equals(selector)) {
+ return; // Universal selector.
+ }
+ int voiceStartIndex = selector.indexOf('[');
+ if (voiceStartIndex != -1) {
+ Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex));
+ if (matcher.matches()) {
+ style.setTargetVoice(matcher.group(1));
+ }
+ selector = selector.substring(0, voiceStartIndex);
+ }
+ String[] classDivision = selector.split("\\.");
+ String tagAndIdDivision = classDivision[0];
+ int idPrefixIndex = tagAndIdDivision.indexOf('#');
+ if (idPrefixIndex != -1) {
+ style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex));
+ style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'.
+ } else {
+ style.setTargetTagName(tagAndIdDivision);
+ }
+ if (classDivision.length > 1) {
+ style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length));
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.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 com.google.android.exoplayer2.text.webvtt;
+
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file.
+ */
+public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
+
+ private static final int BOX_HEADER_SIZE = 8;
+
+ private static final int TYPE_payl = Util.getIntegerCodeForString("payl");
+ private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg");
+ private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc");
+
+ private final ParsableByteArray sampleData;
+ private final WebvttCue.Builder builder;
+
+ public Mp4WebvttDecoder() {
+ super("Mp4WebvttDecoder");
+ sampleData = new ParsableByteArray();
+ builder = new WebvttCue.Builder();
+ }
+
+ @Override
+ protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:
+ // first 4 bytes size and then 4 bytes type.
+ sampleData.reset(bytes, length);
+ List<Cue> resultingCueList = new ArrayList<>();
+ while (sampleData.bytesLeft() > 0) {
+ if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
+ throw new SubtitleDecoderException("Incomplete Mp4Webvtt Top Level box header found.");
+ }
+ int boxSize = sampleData.readInt();
+ int boxType = sampleData.readInt();
+ if (boxType == TYPE_vttc) {
+ resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE));
+ } else {
+ // Peers of the VTTCueBox are still not supported and are skipped.
+ sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
+ }
+ }
+ return new Mp4WebvttSubtitle(resultingCueList);
+ }
+
+ private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder,
+ int remainingCueBoxBytes) throws SubtitleDecoderException {
+ builder.reset();
+ while (remainingCueBoxBytes > 0) {
+ if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
+ throw new SubtitleDecoderException("Incomplete vtt cue box header found.");
+ }
+ int boxSize = sampleData.readInt();
+ int boxType = sampleData.readInt();
+ remainingCueBoxBytes -= BOX_HEADER_SIZE;
+ int payloadLength = boxSize - BOX_HEADER_SIZE;
+ String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength);
+ sampleData.skipBytes(payloadLength);
+ remainingCueBoxBytes -= payloadLength;
+ if (boxType == TYPE_sttg) {
+ WebvttCueParser.parseCueSettingsList(boxPayload, builder);
+ } else if (boxType == TYPE_payl) {
+ WebvttCueParser.parseCueText(null, boxPayload.trim(), builder,
+ Collections.<WebvttCssStyle>emptyList());
+ } else {
+ // Other VTTCueBox children are still not supported and are ignored.
+ }
+ }
+ return builder.build();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Representation of a Webvtt subtitle embedded in a MP4 container file.
+ */
+/* package */ final class Mp4WebvttSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ public Mp4WebvttSubtitle(List<Cue> cueList) {
+ cues = Collections.unmodifiableList(cueList);
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.<Cue>emptyList();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
@@ -0,0 +1,304 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import android.graphics.Typeface;
+import android.support.annotation.IntDef;
+import android.text.Layout;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Style object of a Css style block in a Webvtt file.
+ *
+ * @see <a href="https://w3c.github.io/webvtt/#applying-css-properties">W3C specification - Apply
+ * CSS properties</a>
+ */
+/* package */ final class WebvttCssStyle {
+
+ public static final int UNSPECIFIED = -1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC,
+ STYLE_BOLD_ITALIC})
+ public @interface StyleFlags {}
+ public static final int STYLE_NORMAL = Typeface.NORMAL;
+ public static final int STYLE_BOLD = Typeface.BOLD;
+ public static final int STYLE_ITALIC = Typeface.ITALIC;
+ public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})
+ public @interface FontSizeUnit {}
+ public static final int FONT_SIZE_UNIT_PIXEL = 1;
+ public static final int FONT_SIZE_UNIT_EM = 2;
+ public static final int FONT_SIZE_UNIT_PERCENT = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, OFF, ON})
+ private @interface OptionalBoolean {}
+ private static final int OFF = 0;
+ private static final int ON = 1;
+
+ // Selector properties.
+ private String targetId;
+ private String targetTag;
+ private List<String> targetClasses;
+ private String targetVoice;
+
+ // Style properties.
+ private String fontFamily;
+ private int fontColor;
+ private boolean hasFontColor;
+ private int backgroundColor;
+ private boolean hasBackgroundColor;
+ @OptionalBoolean private int linethrough;
+ @OptionalBoolean private int underline;
+ @OptionalBoolean private int bold;
+ @OptionalBoolean private int italic;
+ @FontSizeUnit private int fontSizeUnit;
+ private float fontSize;
+ private Layout.Alignment textAlign;
+
+ public WebvttCssStyle() {
+ reset();
+ }
+
+ public void reset() {
+ targetId = "";
+ targetTag = "";
+ targetClasses = Collections.emptyList();
+ targetVoice = "";
+ fontFamily = null;
+ hasFontColor = false;
+ hasBackgroundColor = false;
+ linethrough = UNSPECIFIED;
+ underline = UNSPECIFIED;
+ bold = UNSPECIFIED;
+ italic = UNSPECIFIED;
+ fontSizeUnit = UNSPECIFIED;
+ textAlign = null;
+ }
+
+ public void setTargetId(String targetId) {
+ this.targetId = targetId;
+ }
+
+ public void setTargetTagName(String targetTag) {
+ this.targetTag = targetTag;
+ }
+
+ public void setTargetClasses(String[] targetClasses) {
+ this.targetClasses = Arrays.asList(targetClasses);
+ }
+
+ public void setTargetVoice(String targetVoice) {
+ this.targetVoice = targetVoice;
+ }
+
+ /**
+ * Returns a value in a score system compliant with the CSS Specificity rules.
+ *
+ * @see <a href="https://www.w3.org/TR/CSS2/cascade.html">CSS Cascading</a>
+ *
+ * The score works as follows:
+ * <ul>
+ * <li> Id match adds 0x40000000 to the score.
+ * <li> Each class and voice match adds 4 to the score.
+ * <li> Tag matching adds 2 to the score.
+ * <li> Universal selector matching scores 1.
+ * </ul>
+ *
+ * @param id The id of the cue if present, {@code null} otherwise.
+ * @param tag Name of the tag, {@code null} if it refers to the entire cue.
+ * @param classes An array containing the classes the tag belongs to. Must not be null.
+ * @param voice Annotated voice if present, {@code null} otherwise.
+ * @return The score of the match, zero if there is no match.
+ */
+ public int getSpecificityScore(String id, String tag, String[] classes, String voice) {
+ if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()
+ && targetVoice.isEmpty()) {
+ // The selector is universal. It matches with the minimum score if and only if the given
+ // element is a whole cue.
+ return tag.isEmpty() ? 1 : 0;
+ }
+ int score = 0;
+ score = updateScoreForMatch(score, targetId, id, 0x40000000);
+ score = updateScoreForMatch(score, targetTag, tag, 2);
+ score = updateScoreForMatch(score, targetVoice, voice, 4);
+ if (score == -1 || !Arrays.asList(classes).containsAll(targetClasses)) {
+ return 0;
+ } else {
+ score += targetClasses.size() * 4;
+ }
+ return score;
+ }
+
+ /**
+ * Returns the style or {@link #UNSPECIFIED} when no style information is given.
+ *
+ * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
+ * or {@link #STYLE_BOLD_ITALIC}.
+ */
+ @StyleFlags public int getStyle() {
+ if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
+ return UNSPECIFIED;
+ }
+ return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)
+ | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);
+ }
+
+ public boolean isLinethrough() {
+ return linethrough == ON;
+ }
+
+ public WebvttCssStyle setLinethrough(boolean linethrough) {
+ this.linethrough = linethrough ? ON : OFF;
+ return this;
+ }
+
+ public boolean isUnderline() {
+ return underline == ON;
+ }
+
+ public WebvttCssStyle setUnderline(boolean underline) {
+ this.underline = underline ? ON : OFF;
+ return this;
+ }
+ public WebvttCssStyle setBold(boolean bold) {
+ this.bold = bold ? ON : OFF;
+ return this;
+ }
+
+ public WebvttCssStyle setItalic(boolean italic) {
+ this.italic = italic ? ON : OFF;
+ return this;
+ }
+
+ public String getFontFamily() {
+ return fontFamily;
+ }
+
+ public WebvttCssStyle setFontFamily(String fontFamily) {
+ this.fontFamily = Util.toLowerInvariant(fontFamily);
+ return this;
+ }
+
+ public int getFontColor() {
+ if (!hasFontColor) {
+ throw new IllegalStateException("Font color not defined");
+ }
+ return fontColor;
+ }
+
+ public WebvttCssStyle setFontColor(int color) {
+ this.fontColor = color;
+ hasFontColor = true;
+ return this;
+ }
+
+ public boolean hasFontColor() {
+ return hasFontColor;
+ }
+
+ public int getBackgroundColor() {
+ if (!hasBackgroundColor) {
+ throw new IllegalStateException("Background color not defined.");
+ }
+ return backgroundColor;
+ }
+
+ public WebvttCssStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ hasBackgroundColor = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColor() {
+ return hasBackgroundColor;
+ }
+
+ public Layout.Alignment getTextAlign() {
+ return textAlign;
+ }
+
+ public WebvttCssStyle setTextAlign(Layout.Alignment textAlign) {
+ this.textAlign = textAlign;
+ return this;
+ }
+
+ public WebvttCssStyle setFontSize(float fontSize) {
+ this.fontSize = fontSize;
+ return this;
+ }
+
+ public WebvttCssStyle setFontSizeUnit(short unit) {
+ this.fontSizeUnit = unit;
+ return this;
+ }
+
+ @FontSizeUnit public int getFontSizeUnit() {
+ return fontSizeUnit;
+ }
+
+ public float getFontSize() {
+ return fontSize;
+ }
+
+ public void cascadeFrom(WebvttCssStyle style) {
+ if (style.hasFontColor) {
+ setFontColor(style.fontColor);
+ }
+ if (style.bold != UNSPECIFIED) {
+ bold = style.bold;
+ }
+ if (style.italic != UNSPECIFIED) {
+ italic = style.italic;
+ }
+ if (style.fontFamily != null) {
+ fontFamily = style.fontFamily;
+ }
+ if (linethrough == UNSPECIFIED) {
+ linethrough = style.linethrough;
+ }
+ if (underline == UNSPECIFIED) {
+ underline = style.underline;
+ }
+ if (textAlign == null) {
+ textAlign = style.textAlign;
+ }
+ if (fontSizeUnit == UNSPECIFIED) {
+ fontSizeUnit = style.fontSizeUnit;
+ fontSize = style.fontSize;
+ }
+ if (style.hasBackgroundColor) {
+ setBackgroundColor(style.backgroundColor);
+ }
+ }
+
+ private static int updateScoreForMatch(int currentScore, String target, String actual,
+ int score) {
+ if (target.isEmpty() || currentScore == -1) {
+ return currentScore;
+ }
+ return target.equals(actual) ? currentScore + score : -1;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java
@@ -0,0 +1,181 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import android.text.Layout.Alignment;
+import android.text.SpannableStringBuilder;
+import android.util.Log;
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A representation of a WebVTT cue.
+ */
+/* package */ final class WebvttCue extends Cue {
+
+ public final long startTime;
+ public final long endTime;
+
+ public WebvttCue(CharSequence text) {
+ this(0, 0, text);
+ }
+
+ public WebvttCue(long startTime, long endTime, CharSequence text) {
+ this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
+ Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
+ }
+
+ public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
+ float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float position,
+ @Cue.AnchorType int positionAnchor, float width) {
+ super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ /**
+ * Returns whether or not this cue should be placed in the default position and rolled-up with
+ * the other "normal" cues.
+ *
+ * @return Whether this cue should be placed in the default position.
+ */
+ public boolean isNormalCue() {
+ return (line == DIMEN_UNSET && position == DIMEN_UNSET);
+ }
+
+ /**
+ * Builder for WebVTT cues.
+ */
+ @SuppressWarnings("hiding")
+ public static final class Builder {
+
+ private static final String TAG = "WebvttCueBuilder";
+
+ private long startTime;
+ private long endTime;
+ private SpannableStringBuilder text;
+ private Alignment textAlignment;
+ private float line;
+ private int lineType;
+ private int lineAnchor;
+ private float position;
+ private int positionAnchor;
+ private float width;
+
+ // Initialization methods
+
+ public Builder() {
+ reset();
+ }
+
+ public void reset() {
+ startTime = 0;
+ endTime = 0;
+ text = null;
+ textAlignment = null;
+ line = Cue.DIMEN_UNSET;
+ lineType = Cue.TYPE_UNSET;
+ lineAnchor = Cue.TYPE_UNSET;
+ position = Cue.DIMEN_UNSET;
+ positionAnchor = Cue.TYPE_UNSET;
+ width = Cue.DIMEN_UNSET;
+ }
+
+ // Construction methods.
+
+ public WebvttCue build() {
+ if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) {
+ derivePositionAnchorFromAlignment();
+ }
+ return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor,
+ position, positionAnchor, width);
+ }
+
+ public Builder setStartTime(long time) {
+ startTime = time;
+ return this;
+ }
+
+ public Builder setEndTime(long time) {
+ endTime = time;
+ return this;
+ }
+
+ public Builder setText(SpannableStringBuilder aText) {
+ text = aText;
+ return this;
+ }
+
+ public Builder setTextAlignment(Alignment textAlignment) {
+ this.textAlignment = textAlignment;
+ return this;
+ }
+
+ public Builder setLine(float line) {
+ this.line = line;
+ return this;
+ }
+
+ public Builder setLineType(int lineType) {
+ this.lineType = lineType;
+ return this;
+ }
+
+ public Builder setLineAnchor(int lineAnchor) {
+ this.lineAnchor = lineAnchor;
+ return this;
+ }
+
+ public Builder setPosition(float position) {
+ this.position = position;
+ return this;
+ }
+
+ public Builder setPositionAnchor(int positionAnchor) {
+ this.positionAnchor = positionAnchor;
+ return this;
+ }
+
+ public Builder setWidth(float width) {
+ this.width = width;
+ return this;
+ }
+
+ private Builder derivePositionAnchorFromAlignment() {
+ if (textAlignment == null) {
+ positionAnchor = Cue.TYPE_UNSET;
+ } else {
+ switch (textAlignment) {
+ case ALIGN_NORMAL:
+ positionAnchor = Cue.ANCHOR_TYPE_START;
+ break;
+ case ALIGN_CENTER:
+ positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+ break;
+ case ALIGN_OPPOSITE:
+ positionAnchor = Cue.ANCHOR_TYPE_END;
+ break;
+ default:
+ Log.w(TAG, "Unrecognized alignment: " + textAlignment);
+ positionAnchor = Cue.ANCHOR_TYPE_START;
+ break;
+ }
+ }
+ return this;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
@@ -0,0 +1,532 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import android.graphics.Typeface;
+import android.support.annotation.NonNull;
+import android.text.Layout.Alignment;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+import android.util.Log;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues)
+ */
+/* package */ final class WebvttCueParser {
+
+ public static final Pattern CUE_HEADER_PATTERN = Pattern
+ .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
+
+ private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)");
+
+ private static final char CHAR_LESS_THAN = '<';
+ private static final char CHAR_GREATER_THAN = '>';
+ private static final char CHAR_SLASH = '/';
+ private static final char CHAR_AMPERSAND = '&';
+ private static final char CHAR_SEMI_COLON = ';';
+ private static final char CHAR_SPACE = ' ';
+
+ private static final String ENTITY_LESS_THAN = "lt";
+ private static final String ENTITY_GREATER_THAN = "gt";
+ private static final String ENTITY_AMPERSAND = "amp";
+ private static final String ENTITY_NON_BREAK_SPACE = "nbsp";
+
+ private static final String TAG_BOLD = "b";
+ private static final String TAG_ITALIC = "i";
+ private static final String TAG_UNDERLINE = "u";
+ private static final String TAG_CLASS = "c";
+ private static final String TAG_VOICE = "v";
+ private static final String TAG_LANG = "lang";
+
+ private static final int STYLE_BOLD = Typeface.BOLD;
+ private static final int STYLE_ITALIC = Typeface.ITALIC;
+
+ private static final String TAG = "WebvttCueParser";
+
+ private final StringBuilder textBuilder;
+
+ public WebvttCueParser() {
+ textBuilder = new StringBuilder();
+ }
+
+ /**
+ * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.
+ *
+ * @param webvttData Parsable WebVTT file data.
+ * @param builder Builder for WebVTT Cues.
+ * @param styles List of styles defined by the CSS style blocks preceeding the cues.
+ * @return Whether a valid Cue was found.
+ */
+ /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder,
+ List<WebvttCssStyle> styles) {
+ String firstLine = webvttData.readLine();
+ Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
+ if (cueHeaderMatcher.matches()) {
+ // We have found the timestamps in the first line. No id present.
+ return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);
+ } else {
+ // The first line is not the timestamps, but could be the cue id.
+ String secondLine = webvttData.readLine();
+ cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
+ if (cueHeaderMatcher.matches()) {
+ // We can do the rest of the parsing, including the id.
+ return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
+ styles);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parses a string containing a list of cue settings.
+ *
+ * @param cueSettingsList String containing the settings for a given cue.
+ * @param builder The {@link WebvttCue.Builder} where incremental construction takes place.
+ */
+ /* package */ static void parseCueSettingsList(String cueSettingsList,
+ WebvttCue.Builder builder) {
+ // Parse the cue settings list.
+ Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);
+ while (cueSettingMatcher.find()) {
+ String name = cueSettingMatcher.group(1);
+ String value = cueSettingMatcher.group(2);
+ try {
+ if ("line".equals(name)) {
+ parseLineAttribute(value, builder);
+ } else if ("align".equals(name)) {
+ builder.setTextAlignment(parseTextAlignment(value));
+ } else if ("position".equals(name)) {
+ parsePositionAttribute(value, builder);
+ } else if ("size".equals(name)) {
+ builder.setWidth(WebvttParserUtil.parsePercentage(value));
+ } else {
+ Log.w(TAG, "Unknown cue setting " + name + ":" + value);
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group());
+ }
+ }
+ }
+
+ /**
+ * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
+ *
+ * @param id Id of the cue, {@code null} if it is not present.
+ * @param markup The markup text to be parsed.
+ * @param styles List of styles defined by the CSS style blocks preceeding the cues.
+ * @param builder Output builder.
+ */
+ /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
+ List<WebvttCssStyle> styles) {
+ SpannableStringBuilder spannedText = new SpannableStringBuilder();
+ Stack<StartTag> startTagStack = new Stack<>();
+ List<StyleMatch> scratchStyleMatches = new ArrayList<>();
+ int pos = 0;
+ while (pos < markup.length()) {
+ char curr = markup.charAt(pos);
+ switch (curr) {
+ case CHAR_LESS_THAN:
+ if (pos + 1 >= markup.length()) {
+ pos++;
+ break; // avoid ArrayOutOfBoundsException
+ }
+ int ltPos = pos;
+ boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH;
+ pos = findEndOfTag(markup, ltPos + 1);
+ boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH;
+ String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1),
+ isVoidTag ? pos - 2 : pos - 1);
+ String tagName = getTagName(fullTagExpression);
+ if (tagName == null || !isSupportedTag(tagName)) {
+ continue;
+ }
+ if (isClosingTag) {
+ StartTag startTag;
+ do {
+ if (startTagStack.isEmpty()) {
+ break;
+ }
+ startTag = startTagStack.pop();
+ applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);
+ } while(!startTag.name.equals(tagName));
+ } else if (!isVoidTag) {
+ startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
+ }
+ break;
+ case CHAR_AMPERSAND:
+ int semiColonEndIndex = markup.indexOf(CHAR_SEMI_COLON, pos + 1);
+ int spaceEndIndex = markup.indexOf(CHAR_SPACE, pos + 1);
+ int entityEndIndex = semiColonEndIndex == -1 ? spaceEndIndex
+ : (spaceEndIndex == -1 ? semiColonEndIndex
+ : Math.min(semiColonEndIndex, spaceEndIndex));
+ if (entityEndIndex != -1) {
+ applyEntity(markup.substring(pos + 1, entityEndIndex), spannedText);
+ if (entityEndIndex == spaceEndIndex) {
+ spannedText.append(" ");
+ }
+ pos = entityEndIndex + 1;
+ } else {
+ spannedText.append(curr);
+ pos++;
+ }
+ break;
+ default:
+ spannedText.append(curr);
+ pos++;
+ break;
+ }
+ }
+ // apply unclosed tags
+ while (!startTagStack.isEmpty()) {
+ applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);
+ }
+ applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
+ scratchStyleMatches);
+ builder.setText(spannedText);
+ }
+
+ private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData,
+ WebvttCue.Builder builder, StringBuilder textBuilder, List<WebvttCssStyle> styles) {
+ try {
+ // Parse the cue start and end times.
+ builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
+ .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group());
+ return false;
+ }
+
+ parseCueSettingsList(cueHeaderMatcher.group(3), builder);
+
+ // Parse the cue text.
+ textBuilder.setLength(0);
+ String line;
+ while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append("\n");
+ }
+ textBuilder.append(line.trim());
+ }
+ parseCueText(id, textBuilder.toString(), builder, styles);
+ return true;
+ }
+
+ // Internal methods
+
+ private static void parseLineAttribute(String s, WebvttCue.Builder builder)
+ throws NumberFormatException {
+ int commaIndex = s.indexOf(',');
+ if (commaIndex != -1) {
+ builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
+ s = s.substring(0, commaIndex);
+ } else {
+ builder.setLineAnchor(Cue.TYPE_UNSET);
+ }
+ if (s.endsWith("%")) {
+ builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);
+ } else {
+ int lineNumber = Integer.parseInt(s);
+ if (lineNumber < 0) {
+ // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as
+ // Cue defines it to be the first row that's not visible.
+ lineNumber--;
+ }
+ builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER);
+ }
+ }
+
+ private static void parsePositionAttribute(String s, WebvttCue.Builder builder)
+ throws NumberFormatException {
+ int commaIndex = s.indexOf(',');
+ if (commaIndex != -1) {
+ builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
+ s = s.substring(0, commaIndex);
+ } else {
+ builder.setPositionAnchor(Cue.TYPE_UNSET);
+ }
+ builder.setPosition(WebvttParserUtil.parsePercentage(s));
+ }
+
+ private static int parsePositionAnchor(String s) {
+ switch (s) {
+ case "start":
+ return Cue.ANCHOR_TYPE_START;
+ case "center":
+ case "middle":
+ return Cue.ANCHOR_TYPE_MIDDLE;
+ case "end":
+ return Cue.ANCHOR_TYPE_END;
+ default:
+ Log.w(TAG, "Invalid anchor value: " + s);
+ return Cue.TYPE_UNSET;
+ }
+ }
+
+ private static Alignment parseTextAlignment(String s) {
+ switch (s) {
+ case "start":
+ case "left":
+ return Alignment.ALIGN_NORMAL;
+ case "center":
+ case "middle":
+ return Alignment.ALIGN_CENTER;
+ case "end":
+ case "right":
+ return Alignment.ALIGN_OPPOSITE;
+ default:
+ Log.w(TAG, "Invalid alignment value: " + s);
+ return null;
+ }
+ }
+
+ /**
+ * Find end of tag (>). The position returned is the position of the > plus one (exclusive).
+ *
+ * @param markup The WebVTT cue markup to be parsed.
+ * @param startPos The position from where to start searching for the end of tag.
+ * @return The position of the end of tag plus 1 (one).
+ */
+ private static int findEndOfTag(String markup, int startPos) {
+ int index = markup.indexOf(CHAR_GREATER_THAN, startPos);
+ return index == -1 ? markup.length() : index + 1;
+ }
+
+ private static void applyEntity(String entity, SpannableStringBuilder spannedText) {
+ switch (entity) {
+ case ENTITY_LESS_THAN:
+ spannedText.append('<');
+ break;
+ case ENTITY_GREATER_THAN:
+ spannedText.append('>');
+ break;
+ case ENTITY_NON_BREAK_SPACE:
+ spannedText.append(' ');
+ break;
+ case ENTITY_AMPERSAND:
+ spannedText.append('&');
+ break;
+ default:
+ Log.w(TAG, "ignoring unsupported entity: '&" + entity + ";'");
+ break;
+ }
+ }
+
+ private static boolean isSupportedTag(String tagName) {
+ switch (tagName) {
+ case TAG_BOLD:
+ case TAG_CLASS:
+ case TAG_ITALIC:
+ case TAG_LANG:
+ case TAG_UNDERLINE:
+ case TAG_VOICE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text,
+ List<WebvttCssStyle> styles, List<StyleMatch> scratchStyleMatches) {
+ int start = startTag.position;
+ int end = text.length();
+ switch(startTag.name) {
+ case TAG_BOLD:
+ text.setSpan(new StyleSpan(STYLE_BOLD), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_ITALIC:
+ text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_UNDERLINE:
+ text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_CLASS:
+ case TAG_LANG:
+ case TAG_VOICE:
+ case "": // Case of the "whole cue" virtual tag.
+ break;
+ default:
+ return;
+ }
+ scratchStyleMatches.clear();
+ getApplicableStyles(styles, cueId, startTag, scratchStyleMatches);
+ int styleMatchesCount = scratchStyleMatches.size();
+ for (int i = 0; i < styleMatchesCount; i++) {
+ applyStyleToText(text, scratchStyleMatches.get(i).style, start, end);
+ }
+ }
+
+ private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style,
+ int start, int end) {
+ if (style == null) {
+ return;
+ }
+ if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
+ spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getTextAlign() != null) {
+ spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ switch (style.getFontSizeUnit()) {
+ case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
+ spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_EM:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.UNSPECIFIED:
+ // Do nothing.
+ break;
+ }
+ }
+
+ /**
+ * Returns the tag name for the given tag contents.
+ *
+ * @param tagExpression Characters between &lt: and &gt; of a start or end tag.
+ * @return The name of tag.
+ */
+ private static String getTagName(String tagExpression) {
+ tagExpression = tagExpression.trim();
+ if (tagExpression.isEmpty()) {
+ return null;
+ }
+ return tagExpression.split("[ \\.]")[0];
+ }
+
+ private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,
+ StartTag tag, List<StyleMatch> output) {
+ int styleCount = declaredStyles.size();
+ for (int i = 0; i < styleCount; i++) {
+ WebvttCssStyle style = declaredStyles.get(i);
+ int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice);
+ if (score > 0) {
+ output.add(new StyleMatch(score, style));
+ }
+ }
+ Collections.sort(output);
+ }
+
+ private static final class StyleMatch implements Comparable<StyleMatch> {
+
+ public final int score;
+ public final WebvttCssStyle style;
+
+ public StyleMatch(int score, WebvttCssStyle style) {
+ this.score = score;
+ this.style = style;
+ }
+
+ @Override
+ public int compareTo(@NonNull StyleMatch another) {
+ return this.score - another.score;
+ }
+
+ }
+
+ private static final class StartTag {
+
+ private static final String[] NO_CLASSES = new String[0];
+
+ public final String name;
+ public final int position;
+ public final String voice;
+ public final String[] classes;
+
+ private StartTag(String name, int position, String voice, String[] classes) {
+ this.position = position;
+ this.name = name;
+ this.voice = voice;
+ this.classes = classes;
+ }
+
+ public static StartTag buildStartTag(String fullTagExpression, int position) {
+ fullTagExpression = fullTagExpression.trim();
+ if (fullTagExpression.isEmpty()) {
+ return null;
+ }
+ int voiceStartIndex = fullTagExpression.indexOf(" ");
+ String voice;
+ if (voiceStartIndex == -1) {
+ voice = "";
+ } else {
+ voice = fullTagExpression.substring(voiceStartIndex).trim();
+ fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
+ }
+ String[] nameAndClasses = fullTagExpression.split("\\.");
+ String name = nameAndClasses[0];
+ String[] classes;
+ if (nameAndClasses.length > 1) {
+ classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length);
+ } else {
+ classes = NO_CLASSES;
+ }
+ return new StartTag(name, position, voice, classes);
+ }
+
+ public static StartTag buildWholeCueVirtualTag() {
+ return new StartTag("", 0, "", new String[0]);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for WebVTT.
+ * <p>
+ * @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
+ */
+public final class WebvttDecoder extends SimpleSubtitleDecoder {
+
+ private static final int EVENT_NONE = -1;
+ private static final int EVENT_END_OF_FILE = 0;
+ private static final int EVENT_COMMENT = 1;
+ private static final int EVENT_STYLE_BLOCK = 2;
+ private static final int EVENT_CUE = 3;
+
+ private static final String COMMENT_START = "NOTE";
+ private static final String STYLE_START = "STYLE";
+
+ private final WebvttCueParser cueParser;
+ private final ParsableByteArray parsableWebvttData;
+ private final WebvttCue.Builder webvttCueBuilder;
+ private final CssParser cssParser;
+ private final List<WebvttCssStyle> definedStyles;
+
+ public WebvttDecoder() {
+ super("WebvttDecoder");
+ cueParser = new WebvttCueParser();
+ parsableWebvttData = new ParsableByteArray();
+ webvttCueBuilder = new WebvttCue.Builder();
+ cssParser = new CssParser();
+ definedStyles = new ArrayList<>();
+ }
+
+ @Override
+ protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ parsableWebvttData.reset(bytes, length);
+ // Initialization for consistent starting state.
+ webvttCueBuilder.reset();
+ definedStyles.clear();
+
+ // Validate the first line of the header, and skip the remainder.
+ WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
+ while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+
+ int event;
+ ArrayList<WebvttCue> subtitles = new ArrayList<>();
+ while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) {
+ if (event == EVENT_COMMENT) {
+ skipComment(parsableWebvttData);
+ } else if (event == EVENT_STYLE_BLOCK) {
+ if (!subtitles.isEmpty()) {
+ throw new SubtitleDecoderException("A style block was found after the first cue.");
+ }
+ parsableWebvttData.readLine(); // Consume the "STYLE" header.
+ WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
+ if (styleBlock != null) {
+ definedStyles.add(styleBlock);
+ }
+ } else if (event == EVENT_CUE) {
+ if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
+ subtitles.add(webvttCueBuilder.build());
+ webvttCueBuilder.reset();
+ }
+ }
+ }
+ return new WebvttSubtitle(subtitles);
+ }
+
+ /**
+ * Positions the input right before the next event, and returns the kind of event found. Does not
+ * consume any data from such event, if any.
+ *
+ * @return The kind of event found.
+ */
+ private static int getNextEvent(ParsableByteArray parsableWebvttData) {
+ int foundEvent = EVENT_NONE;
+ int currentInputPosition = 0;
+ while (foundEvent == EVENT_NONE) {
+ currentInputPosition = parsableWebvttData.getPosition();
+ String line = parsableWebvttData.readLine();
+ if (line == null) {
+ foundEvent = EVENT_END_OF_FILE;
+ } else if (STYLE_START.equals(line)) {
+ foundEvent = EVENT_STYLE_BLOCK;
+ } else if (COMMENT_START.startsWith(line)) {
+ foundEvent = EVENT_COMMENT;
+ } else {
+ foundEvent = EVENT_CUE;
+ }
+ }
+ parsableWebvttData.setPosition(currentInputPosition);
+ return foundEvent;
+ }
+
+ private static void skipComment(ParsableByteArray parsableWebvttData) {
+ while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.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 com.google.android.exoplayer2.text.webvtt;
+
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for parsing WebVTT data.
+ */
+public final class WebvttParserUtil {
+
+ private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$");
+ private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$");
+
+ private WebvttParserUtil() {}
+
+ /**
+ * Reads and validates the first line of a WebVTT file.
+ *
+ * @param input The input from which the line should be read.
+ * @throws SubtitleDecoderException If the line isn't the start of a valid WebVTT file.
+ */
+ public static void validateWebvttHeaderLine(ParsableByteArray input)
+ throws SubtitleDecoderException {
+ String line = input.readLine();
+ if (line == null || !HEADER.matcher(line).matches()) {
+ throw new SubtitleDecoderException("Expected WEBVTT. Got " + line);
+ }
+ }
+
+ /**
+ * Parses a WebVTT timestamp.
+ *
+ * @param timestamp The timestamp string.
+ * @return The parsed timestamp in microseconds.
+ * @throws NumberFormatException If the timestamp could not be parsed.
+ */
+ public static long parseTimestampUs(String timestamp) throws NumberFormatException {
+ long value = 0;
+ String[] parts = timestamp.split("\\.", 2);
+ String[] subparts = parts[0].split(":");
+ for (String subpart : subparts) {
+ value = value * 60 + Long.parseLong(subpart);
+ }
+ return (value * 1000 + Long.parseLong(parts[1])) * 1000;
+ }
+
+ /**
+ * Parses a percentage string.
+ *
+ * @param s The percentage string.
+ * @return The parsed value, where 1.0 represents 100%.
+ * @throws NumberFormatException If the percentage could not be parsed.
+ */
+ public static float parsePercentage(String s) throws NumberFormatException {
+ if (!s.endsWith("%")) {
+ throw new NumberFormatException("Percentages must end with %");
+ }
+ return Float.parseFloat(s.substring(0, s.length() - 1)) / 100;
+ }
+
+ /**
+ * Reads lines up to and including the next WebVTT cue header.
+ *
+ * @param input The input from which lines should be read.
+ * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was
+ * reached without a cue header being found. In the case that a cue header is found, groups 1,
+ * 2 and 3 of the returned matcher contain the start time, end time and settings list.
+ */
+ public static Matcher findNextCueHeader(ParsableByteArray input) {
+ String line;
+ while ((line = input.readLine()) != null) {
+ if (COMMENT.matcher(line).matches()) {
+ // Skip until the end of the comment block.
+ while ((line = input.readLine()) != null && !line.isEmpty()) {}
+ } else {
+ Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line);
+ if (cueHeaderMatcher.matches()) {
+ return cueHeaderMatcher;
+ }
+ }
+ }
+ return null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.google.android.exoplayer2.text.webvtt;
+
+import android.text.SpannableStringBuilder;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a WebVTT subtitle.
+ */
+/* package */ final class WebvttSubtitle implements Subtitle {
+
+ private final List<WebvttCue> cues;
+ private final int numCues;
+ private final long[] cueTimesUs;
+ private final long[] sortedCueTimesUs;
+
+ /**
+ * @param cues A list of the cues in this subtitle.
+ */
+ public WebvttSubtitle(List<WebvttCue> cues) {
+ this.cues = cues;
+ numCues = cues.size();
+ cueTimesUs = new long[2 * numCues];
+ for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
+ WebvttCue cue = cues.get(cueIndex);
+ int arrayIndex = cueIndex * 2;
+ cueTimesUs[arrayIndex] = cue.startTime;
+ cueTimesUs[arrayIndex + 1] = cue.endTime;
+ }
+ sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
+ Arrays.sort(sortedCueTimesUs);
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);
+ return index < sortedCueTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return sortedCueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < sortedCueTimesUs.length);
+ return sortedCueTimesUs[index];
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ ArrayList<Cue> list = null;
+ WebvttCue firstNormalCue = null;
+ SpannableStringBuilder normalCueTextBuilder = null;
+
+ for (int i = 0; i < numCues; i++) {
+ if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ WebvttCue cue = cues.get(i);
+ if (cue.isNormalCue()) {
+ // we want to merge all of the normal cues into a single cue to ensure they are drawn
+ // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple
+ // normal cues, otherwise we can just append the single normal cue
+ if (firstNormalCue == null) {
+ firstNormalCue = cue;
+ } else if (normalCueTextBuilder == null) {
+ normalCueTextBuilder = new SpannableStringBuilder();
+ normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text);
+ } else {
+ normalCueTextBuilder.append("\n").append(cue.text);
+ }
+ } else {
+ list.add(cue);
+ }
+ }
+ }
+ if (normalCueTextBuilder != null) {
+ // there were multiple normal cues, so create a new cue with all of the text
+ list.add(new WebvttCue(normalCueTextBuilder));
+ } else if (firstNormalCue != null) {
+ // there was only a single normal cue, so just add it to the list
+ list.add(firstNormalCue);
+ }
+
+ if (list != null) {
+ return list;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -0,0 +1,253 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import java.util.List;
+
+/**
+ * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one
+ * of highest quality given the current network conditions and the state of the buffer.
+ */
+public class AdaptiveTrackSelection extends BaseTrackSelection {
+
+ /**
+ * Factory for {@link AdaptiveTrackSelection} instances.
+ */
+ public static final class Factory implements TrackSelection.Factory {
+
+ private final BandwidthMeter bandwidthMeter;
+ private final int maxInitialBitrate;
+ private final int minDurationForQualityIncreaseMs;
+ private final int maxDurationForQualityDecreaseMs;
+ private final int minDurationToRetainAfterDiscardMs;
+ private final float bandwidthFraction;
+
+ /**
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ */
+ public Factory(BandwidthMeter bandwidthMeter) {
+ this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+ DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+ DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+ DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
+ }
+
+ /**
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
+ * when a bandwidth estimate is unavailable.
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
+ * the selected track to switch to one of higher quality.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
+ * the selected track to switch to one of lower quality.
+ * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
+ * quality, the selection may indicate that media already buffered at the lower quality can
+ * be discarded to speed up the switch. This is the minimum duration of media that must be
+ * retained at the lower quality.
+ * @param bandwidthFraction The fraction of the available bandwidth that the selection should
+ * consider available for use. Setting to a value less than 1 is recommended to account
+ * for inaccuracies in the bandwidth estimator.
+ */
+ public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
+ int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs, float bandwidthFraction) {
+ this.bandwidthMeter = bandwidthMeter;
+ this.maxInitialBitrate = maxInitialBitrate;
+ this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
+ this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
+ this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
+ this.bandwidthFraction = bandwidthFraction;
+ }
+
+ @Override
+ public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
+ return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate,
+ minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs, bandwidthFraction);
+ }
+
+ }
+
+ public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
+ public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
+ public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
+ public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
+ public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
+
+ private final BandwidthMeter bandwidthMeter;
+ private final int maxInitialBitrate;
+ private final long minDurationForQualityIncreaseUs;
+ private final long maxDurationForQualityDecreaseUs;
+ private final long minDurationToRetainAfterDiscardUs;
+ private final float bandwidthFraction;
+
+ private int selectedIndex;
+ private int reason;
+
+ /**
+ * @param group The {@link TrackGroup}.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * empty. May be in any order.
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ */
+ public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
+ BandwidthMeter bandwidthMeter) {
+ this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+ DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+ DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+ DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * empty. May be in any order.
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a
+ * bandwidth estimate is unavailable.
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
+ * selected track to switch to one of higher quality.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
+ * selected track to switch to one of lower quality.
+ * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
+ * quality, the selection may indicate that media already buffered at the lower quality can
+ * be discarded to speed up the switch. This is the minimum duration of media that must be
+ * retained at the lower quality.
+ * @param bandwidthFraction The fraction of the available bandwidth that the selection should
+ * consider available for use. Setting to a value less than 1 is recommended to account
+ * for inaccuracies in the bandwidth estimator.
+ */
+ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter,
+ int maxInitialBitrate, long minDurationForQualityIncreaseMs,
+ long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction) {
+ super(group, tracks);
+ this.bandwidthMeter = bandwidthMeter;
+ this.maxInitialBitrate = maxInitialBitrate;
+ this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
+ this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
+ this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
+ this.bandwidthFraction = bandwidthFraction;
+ selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
+ reason = C.SELECTION_REASON_INITIAL;
+ }
+
+ @Override
+ public void updateSelectedTrack(long bufferedDurationUs) {
+ long nowMs = SystemClock.elapsedRealtime();
+ // Get the current and ideal selections.
+ int currentSelectedIndex = selectedIndex;
+ Format currentFormat = getSelectedFormat();
+ int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
+ Format idealFormat = getFormat(idealSelectedIndex);
+ // Assume we can switch to the ideal selection.
+ selectedIndex = idealSelectedIndex;
+ // Revert back to the current selection if conditions are not suitable for switching.
+ if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {
+ if (idealFormat.bitrate > currentFormat.bitrate
+ && bufferedDurationUs < minDurationForQualityIncreaseUs) {
+ // The ideal track is a higher quality, but we have insufficient buffer to safely switch
+ // up. Defer switching up for now.
+ selectedIndex = currentSelectedIndex;
+ } else if (idealFormat.bitrate < currentFormat.bitrate
+ && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
+ // The ideal track is a lower quality, but we have sufficient buffer to defer switching
+ // down for now.
+ selectedIndex = currentSelectedIndex;
+ }
+ }
+ // If we adapted, update the trigger.
+ if (selectedIndex != currentSelectedIndex) {
+ reason = C.SELECTION_REASON_ADAPTIVE;
+ }
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return reason;
+ }
+
+ @Override
+ public Object getSelectionData() {
+ return null;
+ }
+
+ @Override
+ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+ if (queue.isEmpty()) {
+ return 0;
+ }
+ int queueSize = queue.size();
+ long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
+ if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
+ return queueSize;
+ }
+ int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime());
+ Format idealFormat = getFormat(idealSelectedIndex);
+ // If the chunks contain video, discard from the first SD chunk beyond
+ // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal
+ // track.
+ for (int i = 0; i < queueSize; i++) {
+ MediaChunk chunk = queue.get(i);
+ Format format = chunk.trackFormat;
+ long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
+ if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
+ && format.bitrate < idealFormat.bitrate
+ && format.height != Format.NO_VALUE && format.height < 720
+ && format.width != Format.NO_VALUE && format.width < 1280
+ && format.height < idealFormat.height) {
+ return i;
+ }
+ }
+ return queueSize;
+ }
+
+ /**
+ * Computes the ideal selected index ignoring buffer health.
+ *
+ * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or
+ * {@link Long#MIN_VALUE} to ignore blacklisting.
+ */
+ private int determineIdealSelectedIndex(long nowMs) {
+ long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
+ long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
+ ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
+ int lowestBitrateNonBlacklistedIndex = 0;
+ for (int i = 0; i < length; i++) {
+ if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
+ Format format = getFormat(i);
+ if (format.bitrate <= effectiveBitrate) {
+ return i;
+ } else {
+ lowestBitrateNonBlacklistedIndex = i;
+ }
+ }
+ }
+ return lowestBitrateNonBlacklistedIndex;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -0,0 +1,194 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * An abstract base class suitable for most {@link TrackSelection} implementations.
+ */
+public abstract class BaseTrackSelection implements TrackSelection {
+
+ /**
+ * The selected {@link TrackGroup}.
+ */
+ protected final TrackGroup group;
+ /**
+ * The number of selected tracks within the {@link TrackGroup}. Always greater than zero.
+ */
+ protected final int length;
+ /**
+ * The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth.
+ */
+ protected final int[] tracks;
+
+ /**
+ * The {@link Format}s of the selected tracks, in order of decreasing bandwidth.
+ */
+ private final Format[] formats;
+ /**
+ * Selected track blacklist timestamps, in order of decreasing bandwidth.
+ */
+ private final long[] blacklistUntilTimes;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ */
+ public BaseTrackSelection(TrackGroup group, int... tracks) {
+ Assertions.checkState(tracks.length > 0);
+ this.group = Assertions.checkNotNull(group);
+ this.length = tracks.length;
+ // Set the formats, sorted in order of decreasing bandwidth.
+ formats = new Format[length];
+ for (int i = 0; i < tracks.length; i++) {
+ formats[i] = group.getFormat(tracks[i]);
+ }
+ Arrays.sort(formats, new DecreasingBandwidthComparator());
+ // Set the format indices in the same order.
+ this.tracks = new int[length];
+ for (int i = 0; i < length; i++) {
+ this.tracks[i] = group.indexOf(formats[i]);
+ }
+ blacklistUntilTimes = new long[length];
+ }
+
+ @Override
+ public final TrackGroup getTrackGroup() {
+ return group;
+ }
+
+ @Override
+ public final int length() {
+ return tracks.length;
+ }
+
+ @Override
+ public final Format getFormat(int index) {
+ return formats[index];
+ }
+
+ @Override
+ public final int getIndexInTrackGroup(int index) {
+ return tracks[index];
+ }
+
+ @Override
+ public final int indexOf(Format format) {
+ for (int i = 0; i < length; i++) {
+ if (formats[i] == format) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public final int indexOf(int indexInTrackGroup) {
+ for (int i = 0; i < length; i++) {
+ if (tracks[i] == indexInTrackGroup) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public final Format getSelectedFormat() {
+ return formats[getSelectedIndex()];
+ }
+
+ @Override
+ public final int getSelectedIndexInTrackGroup() {
+ return tracks[getSelectedIndex()];
+ }
+
+ @Override
+ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+ return queue.size();
+ }
+
+ @Override
+ public final boolean blacklist(int index, long blacklistDurationMs) {
+ long nowMs = SystemClock.elapsedRealtime();
+ boolean canBlacklist = isBlacklisted(index, nowMs);
+ for (int i = 0; i < length && !canBlacklist; i++) {
+ canBlacklist = i != index && !isBlacklisted(i, nowMs);
+ }
+ if (!canBlacklist) {
+ return false;
+ }
+ blacklistUntilTimes[index] = Math.max(blacklistUntilTimes[index], nowMs + blacklistDurationMs);
+ return true;
+ }
+
+ /**
+ * Returns whether the track at the specified index in the selection is blacklisted.
+ *
+ * @param index The index of the track in the selection.
+ * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}.
+ */
+ protected final boolean isBlacklisted(int index, long nowMs) {
+ return blacklistUntilTimes[index] > nowMs;
+ }
+
+ // Object overrides.
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks);
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ BaseTrackSelection other = (BaseTrackSelection) obj;
+ return group == other.group && Arrays.equals(tracks, other.tracks);
+ }
+
+ /**
+ * Sorts {@link Format} objects in order of decreasing bandwidth.
+ */
+ private static final class DecreasingBandwidthComparator implements Comparator<Format> {
+
+ @Override
+ public int compare(Format a, Format b) {
+ return b.bitrate - a.bitrate;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -0,0 +1,979 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A {@link MappingTrackSelector} that allows configuration of common parameters. It is safe to call
+ * the methods of this class from the application thread. See {@link Parameters#Parameters()} for
+ * default selection parameters.
+ */
+public class DefaultTrackSelector extends MappingTrackSelector {
+
+ /**
+ * Holder for available configurations for the {@link DefaultTrackSelector}.
+ */
+ public static final class Parameters {
+
+ // Audio.
+ public final String preferredAudioLanguage;
+
+ // Text.
+ public final String preferredTextLanguage;
+
+ // Video.
+ public final boolean allowMixedMimeAdaptiveness;
+ public final boolean allowNonSeamlessAdaptiveness;
+ public final int maxVideoWidth;
+ public final int maxVideoHeight;
+ public final int maxVideoBitrate;
+ public final boolean exceedVideoConstraintsIfNecessary;
+ public final boolean exceedRendererCapabilitiesIfNecessary;
+ public final int viewportWidth;
+ public final int viewportHeight;
+ public final boolean orientationMayChange;
+
+ /**
+ * Constructor with default selection parameters:
+ * <ul>
+ * <li>No preferred audio language is set.</li>
+ * <li>No preferred text language is set.</li>
+ * <li>Adaptation between different mime types is not allowed.</li>
+ * <li>Non seamless adaptation is allowed.</li>
+ * <li>No max limit for video width/height.</li>
+ * <li>No max video bitrate.</li>
+ * <li>Video constraints are exceeded if no supported selection can be made otherwise.</li>
+ * <li>Renderer capabilities are exceeded if no supported selection can be made.</li>
+ * <li>No viewport width/height constraints are set.</li>
+ * </ul>
+ */
+ public Parameters() {
+ this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true,
+ true, Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+ }
+
+ /**
+ * @param preferredAudioLanguage The preferred language for audio, as well as for forced text
+ * tracks as defined by RFC 5646. {@code null} to select the default track, or first track
+ * if there's no default.
+ * @param preferredTextLanguage The preferred language for text tracks as defined by RFC 5646.
+ * {@code null} to select the default track, or first track if there's no default.
+ * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types.
+ * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed.
+ * @param maxVideoWidth Maximum allowed video width.
+ * @param maxVideoHeight Maximum allowed video height.
+ * @param maxVideoBitrate Maximum allowed video bitrate.
+ * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
+ * selection can be made otherwise.
+ * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
+ * selection can be made otherwise.
+ * @param viewportWidth Viewport width in pixels.
+ * @param viewportHeight Viewport height in pixels.
+ * @param orientationMayChange Whether orientation may change during playback.
+ */
+ public Parameters(String preferredAudioLanguage, String preferredTextLanguage,
+ boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness,
+ int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate,
+ boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary,
+ int viewportWidth, int viewportHeight, boolean orientationMayChange) {
+ this.preferredAudioLanguage = preferredAudioLanguage;
+ this.preferredTextLanguage = preferredTextLanguage;
+ this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness;
+ this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness;
+ this.maxVideoWidth = maxVideoWidth;
+ this.maxVideoHeight = maxVideoHeight;
+ this.maxVideoBitrate = maxVideoBitrate;
+ this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;
+ this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
+ this.viewportWidth = viewportWidth;
+ this.viewportHeight = viewportHeight;
+ this.orientationMayChange = orientationMayChange;
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided preferred language for audio and
+ * forced text tracks.
+ *
+ * @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to
+ * select the default track, or first track if there's no default.
+ * @return A {@link Parameters} instance with the provided preferred language for audio and
+ * forced text tracks.
+ */
+ public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) {
+ preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage);
+ if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided preferred language for text tracks.
+ *
+ * @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to
+ * select the default track, or no track if there's no default.
+ * @return A {@link Parameters} instance with the provided preferred language for text tracks.
+ */
+ public Parameters withPreferredTextLanguage(String preferredTextLanguage) {
+ preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage);
+ if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided mixed mime adaptiveness allowance.
+ *
+ * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types.
+ * @return A {@link Parameters} instance with the provided mixed mime adaptiveness allowance.
+ */
+ public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) {
+ if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided seamless adaptiveness allowance.
+ *
+ * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed.
+ * @return A {@link Parameters} instance with the provided seamless adaptiveness allowance.
+ */
+ public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) {
+ if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided max video size.
+ *
+ * @param maxVideoWidth The max video width.
+ * @param maxVideoHeight The max video width.
+ * @return A {@link Parameters} instance with the provided max video size.
+ */
+ public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
+ if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided max video bitrate.
+ *
+ * @param maxVideoBitrate The max video bitrate.
+ * @return A {@link Parameters} instance with the provided max video bitrate.
+ */
+ public Parameters withMaxVideoBitrate(int maxVideoBitrate) {
+ if (maxVideoBitrate == this.maxVideoBitrate) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Equivalent to {@code withMaxVideoSize(1279, 719)}.
+ *
+ * @return A {@link Parameters} instance with maximum standard definition as maximum video size.
+ */
+ public Parameters withMaxVideoSizeSd() {
+ return withMaxVideoSize(1279, 719);
+ }
+
+ /**
+ * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}.
+ *
+ * @return A {@link Parameters} instance without video size constraints.
+ */
+ public Parameters withoutVideoSizeConstraints() {
+ return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided
+ * {@code exceedVideoConstraintsIfNecessary} value.
+ *
+ * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
+ * selection can be made otherwise.
+ * @return A {@link Parameters} instance with the provided
+ * {@code exceedVideoConstraintsIfNecessary} value.
+ */
+ public Parameters withExceedVideoConstraintsIfNecessary(
+ boolean exceedVideoConstraintsIfNecessary) {
+ if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided
+ * {@code exceedRendererCapabilitiesIfNecessary} value.
+ *
+ * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
+ * selection can be made otherwise.
+ * @return A {@link Parameters} instance with the provided
+ * {@code exceedRendererCapabilitiesIfNecessary} value.
+ */
+ public Parameters withExceedRendererCapabilitiesIfNecessary(
+ boolean exceedRendererCapabilitiesIfNecessary) {
+ if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance with the provided viewport size.
+ *
+ * @param viewportWidth Viewport width in pixels.
+ * @param viewportHeight Viewport height in pixels.
+ * @param orientationMayChange Whether orientation may change during playback.
+ * @return A {@link Parameters} instance with the provided viewport size.
+ */
+ public Parameters withViewportSize(int viewportWidth, int viewportHeight,
+ boolean orientationMayChange) {
+ if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight
+ && orientationMayChange == this.orientationMayChange) {
+ return this;
+ }
+ return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+ allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary,
+ viewportWidth, viewportHeight, orientationMayChange);
+ }
+
+ /**
+ * Returns a {@link Parameters} instance where the viewport size is obtained from the provided
+ * {@link Context}.
+ *
+ * @param context The context to obtain the viewport size from.
+ * @param orientationMayChange Whether orientation may change during playback.
+ * @return A {@link Parameters} instance where the viewport size is obtained from the provided
+ * {@link Context}.
+ */
+ public Parameters withViewportSizeFromContext(Context context, boolean orientationMayChange) {
+ // Assume the viewport is fullscreen.
+ Point viewportSize = Util.getPhysicalDisplaySize(context);
+ return withViewportSize(viewportSize.x, viewportSize.y, orientationMayChange);
+ }
+
+ /**
+ * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}.
+ *
+ * @return A {@link Parameters} instance without viewport size constraints.
+ */
+ public Parameters withoutViewportSizeConstraints() {
+ return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Parameters other = (Parameters) obj;
+ return allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness
+ && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness
+ && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight
+ && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary
+ && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
+ && orientationMayChange == other.orientationMayChange
+ && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight
+ && maxVideoBitrate == other.maxVideoBitrate
+ && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage)
+ && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = preferredAudioLanguage.hashCode();
+ result = 31 * result + preferredTextLanguage.hashCode();
+ result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0);
+ result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0);
+ result = 31 * result + maxVideoWidth;
+ result = 31 * result + maxVideoHeight;
+ result = 31 * result + maxVideoBitrate;
+ result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0);
+ result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);
+ result = 31 * result + (orientationMayChange ? 1 : 0);
+ result = 31 * result + viewportWidth;
+ result = 31 * result + viewportHeight;
+ return result;
+ }
+
+ }
+
+ /**
+ * If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the
+ * corresponding viewport dimension, then the video is considered as filling the viewport (in that
+ * dimension).
+ */
+ private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f;
+ private static final int[] NO_TRACKS = new int[0];
+ private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
+
+ private final TrackSelection.Factory adaptiveTrackSelectionFactory;
+ private final AtomicReference<Parameters> paramsReference;
+
+ /**
+ * Constructs an instance that does not support adaptive tracks.
+ */
+ public DefaultTrackSelector() {
+ this(null);
+ }
+
+ /**
+ * Constructs an instance that uses a factory to create adaptive track selections.
+ *
+ * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if
+ * the selector should not support adaptive tracks.
+ */
+ public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
+ this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
+ paramsReference = new AtomicReference<>(new Parameters());
+ }
+
+ /**
+ * Atomically sets the provided parameters for track selection.
+ *
+ * @param params The parameters for track selection.
+ */
+ public void setParameters(Parameters params) {
+ Assertions.checkNotNull(params);
+ if (!paramsReference.getAndSet(params).equals(params)) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Gets the current selection parameters.
+ *
+ * @return The current selection parameters.
+ */
+ public Parameters getParameters() {
+ return paramsReference.get();
+ }
+
+ // MappingTrackSelector implementation.
+
+ @Override
+ protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports)
+ throws ExoPlaybackException {
+ // Make a track selection for each renderer.
+ int rendererCount = rendererCapabilities.length;
+ TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount];
+ Parameters params = paramsReference.get();
+ boolean videoTrackAndRendererPresent = false;
+
+ for (int i = 0; i < rendererCount; i++) {
+ if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) {
+ rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
+ rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
+ params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness,
+ params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
+ params.orientationMayChange, adaptiveTrackSelectionFactory,
+ params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary);
+ videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0;
+ }
+ }
+
+ for (int i = 0; i < rendererCount; i++) {
+ switch (rendererCapabilities[i].getTrackType()) {
+ case C.TRACK_TYPE_VIDEO:
+ // Already done. Do nothing.
+ break;
+ case C.TRACK_TYPE_AUDIO:
+ rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
+ rendererFormatSupports[i], params.preferredAudioLanguage,
+ params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness,
+ videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory);
+ break;
+ case C.TRACK_TYPE_TEXT:
+ rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
+ rendererFormatSupports[i], params.preferredTextLanguage,
+ params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
+ break;
+ default:
+ rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(),
+ rendererTrackGroupArrays[i], rendererFormatSupports[i],
+ params.exceedRendererCapabilitiesIfNecessary);
+ break;
+ }
+ }
+ return rendererTrackSelections;
+ }
+
+ // Video track selection implementation.
+
+ protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities,
+ TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
+ int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness,
+ int viewportWidth, int viewportHeight, boolean orientationMayChange,
+ TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary,
+ boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException {
+ TrackSelection selection = null;
+ if (adaptiveTrackSelectionFactory != null) {
+ selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport,
+ maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness,
+ allowMixedMimeAdaptiveness, viewportWidth, viewportHeight,
+ orientationMayChange, adaptiveTrackSelectionFactory);
+ }
+ if (selection == null) {
+ selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange,
+ exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary);
+ }
+ return selection;
+ }
+
+ private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities,
+ TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
+ int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness,
+ int viewportWidth, int viewportHeight, boolean orientationMayChange,
+ TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException {
+ int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness
+ ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)
+ : RendererCapabilities.ADAPTIVE_SEAMLESS;
+ boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness
+ && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0;
+ for (int i = 0; i < groups.length; i++) {
+ TrackGroup group = groups.get(i);
+ int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i],
+ allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange);
+ if (adaptiveTracks.length > 0) {
+ return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks);
+ }
+ }
+ return null;
+ }
+
+ private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport,
+ boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth,
+ int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight,
+ boolean orientationMayChange) {
+ if (group.length < 2) {
+ return NO_TRACKS;
+ }
+
+ List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,
+ viewportHeight, orientationMayChange);
+ if (selectedTrackIndices.size() < 2) {
+ return NO_TRACKS;
+ }
+
+ String selectedMimeType = null;
+ if (!allowMixedMimeTypes) {
+ // Select the mime type for which we have the most adaptive tracks.
+ HashSet<String> seenMimeTypes = new HashSet<>();
+ int selectedMimeTypeTrackCount = 0;
+ for (int i = 0; i < selectedTrackIndices.size(); i++) {
+ int trackIndex = selectedTrackIndices.get(i);
+ String sampleMimeType = group.getFormat(trackIndex).sampleMimeType;
+ if (seenMimeTypes.add(sampleMimeType)) {
+ int countForMimeType = getAdaptiveVideoTrackCountForMimeType(group, formatSupport,
+ requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate, selectedTrackIndices);
+ if (countForMimeType > selectedMimeTypeTrackCount) {
+ selectedMimeType = sampleMimeType;
+ selectedMimeTypeTrackCount = countForMimeType;
+ }
+ }
+ }
+ }
+
+ // Filter by the selected mime type.
+ filterAdaptiveVideoTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport,
+ selectedMimeType, maxVideoWidth, maxVideoHeight, maxVideoBitrate, selectedTrackIndices);
+
+ return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);
+ }
+
+ private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport,
+ int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight,
+ int maxVideoBitrate, List<Integer> selectedTrackIndices) {
+ int adaptiveTrackCount = 0;
+ for (int i = 0; i < selectedTrackIndices.size(); i++) {
+ int trackIndex = selectedTrackIndices.get(i);
+ if (isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType,
+ formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate)) {
+ adaptiveTrackCount++;
+ }
+ }
+ return adaptiveTrackCount;
+ }
+
+ private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group,
+ int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth,
+ int maxVideoHeight, int maxVideoBitrate, List<Integer> selectedTrackIndices) {
+ for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
+ int trackIndex = selectedTrackIndices.get(i);
+ if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType,
+ formatSupport[trackIndex], requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
+ maxVideoBitrate)) {
+ selectedTrackIndices.remove(i);
+ }
+ }
+ }
+
+ private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType,
+ int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight,
+ int maxVideoBitrate) {
+ return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0)
+ && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))
+ && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth)
+ && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight)
+ && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
+ }
+
+ private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups,
+ int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate,
+ int viewportWidth, int viewportHeight, boolean orientationMayChange,
+ boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = 0;
+ int selectedTrackScore = 0;
+ int selectedBitrate = Format.NO_VALUE;
+ int selectedPixelCount = Format.NO_VALUE;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup,
+ viewportWidth, viewportHeight, orientationMayChange);
+ int[] trackFormatSupport = formatSupport[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex)
+ && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth)
+ && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight)
+ && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
+ if (!isWithinConstraints && !exceedConstraintsIfNecessary) {
+ // Track should not be selected.
+ continue;
+ }
+ int trackScore = isWithinConstraints ? 2 : 1;
+ if (isSupported(trackFormatSupport[trackIndex], false)) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ boolean selectTrack = trackScore > selectedTrackScore;
+ if (trackScore == selectedTrackScore) {
+ // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If we're
+ // within constraints prefer a higher pixel count (or bitrate), else prefer a lower
+ // count (or bitrate). If still tied then prefer the first track (i.e. the one that's
+ // already selected).
+ int comparisonResult;
+ int formatPixelCount = format.getPixelCount();
+ if (formatPixelCount != selectedPixelCount) {
+ comparisonResult = compareFormatValues(format.getPixelCount(), selectedPixelCount);
+ } else {
+ comparisonResult = compareFormatValues(format.bitrate, selectedBitrate);
+ }
+ selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0;
+ }
+ if (selectTrack) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ selectedBitrate = format.bitrate;
+ selectedPixelCount = format.getPixelCount();
+ }
+ }
+ }
+ }
+ return selectedGroup == null ? null
+ : new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+ }
+
+ /**
+ * Compares two format values for order. A known value is considered greater than
+ * {@link Format#NO_VALUE}.
+ *
+ * @param first The first value.
+ * @param second The second value.
+ * @return A negative integer if the first value is less than the second. Zero if they are equal.
+ * A positive integer if the first value is greater than the second.
+ */
+ private static int compareFormatValues(int first, int second) {
+ return first == Format.NO_VALUE ? (second == Format.NO_VALUE ? 0 : -1)
+ : (second == Format.NO_VALUE ? 1 : (first - second));
+ }
+
+ // Audio track selection implementation.
+
+ protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport,
+ String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary,
+ boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) {
+ int selectedGroupIndex = C.INDEX_UNSET;
+ int selectedTrackIndex = C.INDEX_UNSET;
+ int selectedTrackScore = 0;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ int[] trackFormatSupport = formatSupport[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex],
+ preferredAudioLanguage, format);
+ if (trackScore > selectedTrackScore) {
+ selectedGroupIndex = groupIndex;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+
+ if (selectedGroupIndex == C.INDEX_UNSET) {
+ return null;
+ }
+
+ TrackGroup selectedGroup = groups.get(selectedGroupIndex);
+ if (adaptiveTrackSelectionFactory != null) {
+ // If the group of the track with the highest score allows it, try to enable adaptation.
+ int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup,
+ formatSupport[selectedGroupIndex], allowMixedMimeAdaptiveness);
+ if (adaptiveTracks.length > 0) {
+ return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup,
+ adaptiveTracks);
+ }
+ }
+ return new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+ }
+
+ private static int getAudioTrackScore(int formatSupport, String preferredLanguage,
+ Format format) {
+ boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+ int trackScore;
+ if (formatHasLanguage(format, preferredLanguage)) {
+ if (isDefault) {
+ trackScore = 4;
+ } else {
+ trackScore = 3;
+ }
+ } else if (isDefault) {
+ trackScore = 2;
+ } else {
+ trackScore = 1;
+ }
+ if (isSupported(formatSupport, false)) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ return trackScore;
+ }
+
+ private static int[] getAdaptiveAudioTracks(TrackGroup group, int[] formatSupport,
+ boolean allowMixedMimeTypes) {
+ int selectedConfigurationTrackCount = 0;
+ AudioConfigurationTuple selectedConfiguration = null;
+ HashSet<AudioConfigurationTuple> seenConfigurationTuples = new HashSet<>();
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ AudioConfigurationTuple configuration = new AudioConfigurationTuple(
+ format.channelCount, format.sampleRate,
+ allowMixedMimeTypes ? null : format.sampleMimeType);
+ if (seenConfigurationTuples.add(configuration)) {
+ int configurationCount = getAdaptiveAudioTrackCount(group, formatSupport, configuration);
+ if (configurationCount > selectedConfigurationTrackCount) {
+ selectedConfiguration = configuration;
+ selectedConfigurationTrackCount = configurationCount;
+ }
+ }
+ }
+
+ if (selectedConfigurationTrackCount > 1) {
+ int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
+ int index = 0;
+ for (int i = 0; i < group.length; i++) {
+ if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i],
+ selectedConfiguration)) {
+ adaptiveIndices[index++] = i;
+ }
+ }
+ return adaptiveIndices;
+ }
+ return NO_TRACKS;
+ }
+
+ private static int getAdaptiveAudioTrackCount(TrackGroup group, int[] formatSupport,
+ AudioConfigurationTuple configuration) {
+ int count = 0;
+ for (int i = 0; i < group.length; i++) {
+ if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], configuration)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static boolean isSupportedAdaptiveAudioTrack(Format format, int formatSupport,
+ AudioConfigurationTuple configuration) {
+ return isSupported(formatSupport, false) && format.channelCount == configuration.channelCount
+ && format.sampleRate == configuration.sampleRate
+ && (configuration.mimeType == null
+ || TextUtils.equals(configuration.mimeType, format.sampleMimeType));
+ }
+
+ // Text track selection implementation.
+
+ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
+ String preferredTextLanguage, String preferredAudioLanguage,
+ boolean exceedRendererCapabilitiesIfNecessary) {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = 0;
+ int selectedTrackScore = 0;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ int[] trackFormatSupport = formatSupport[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+ boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0;
+ int trackScore;
+ if (formatHasLanguage(format, preferredTextLanguage)) {
+ if (isDefault) {
+ trackScore = 6;
+ } else if (!isForced) {
+ // Prefer non-forced to forced if a preferred text language has been specified. Where
+ // both are provided the non-forced track will usually contain the forced subtitles as
+ // a subset.
+ trackScore = 5;
+ } else {
+ trackScore = 4;
+ }
+ } else if (isDefault) {
+ trackScore = 3;
+ } else if (isForced) {
+ if (formatHasLanguage(format, preferredAudioLanguage)) {
+ trackScore = 2;
+ } else {
+ trackScore = 1;
+ }
+ } else {
+ // Track should not be selected.
+ continue;
+ }
+ if (isSupported(trackFormatSupport[trackIndex], false)) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ if (trackScore > selectedTrackScore) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+ return selectedGroup == null ? null
+ : new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+ }
+
+ // General track selection methods.
+
+ protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups,
+ int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = 0;
+ int selectedTrackScore = 0;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ int[] trackFormatSupport = formatSupport[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+ int trackScore = isDefault ? 2 : 1;
+ if (isSupported(trackFormatSupport[trackIndex], false)) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ if (trackScore > selectedTrackScore) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+ return selectedGroup == null ? null
+ : new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+ }
+
+ protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) {
+ int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK;
+ return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities
+ && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES);
+ }
+
+ protected static boolean formatHasLanguage(Format format, String language) {
+ return TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
+ }
+
+ // Viewport size util methods.
+
+ private static List<Integer> getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth,
+ int viewportHeight, boolean orientationMayChange) {
+ // Initially include all indices.
+ ArrayList<Integer> selectedTrackIndices = new ArrayList<>(group.length);
+ for (int i = 0; i < group.length; i++) {
+ selectedTrackIndices.add(i);
+ }
+
+ if (viewportWidth == Integer.MAX_VALUE || viewportHeight == Integer.MAX_VALUE) {
+ // Viewport dimensions not set. Return the full set of indices.
+ return selectedTrackIndices;
+ }
+
+ int maxVideoPixelsToRetain = Integer.MAX_VALUE;
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ // Keep track of the number of pixels of the selected format whose resolution is the
+ // smallest to exceed the maximum size at which it can be displayed within the viewport.
+ // We'll discard formats of higher resolution.
+ if (format.width > 0 && format.height > 0) {
+ Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange,
+ viewportWidth, viewportHeight, format.width, format.height);
+ int videoPixels = format.width * format.height;
+ if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN)
+ && format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN)
+ && videoPixels < maxVideoPixelsToRetain) {
+ maxVideoPixelsToRetain = videoPixels;
+ }
+ }
+ }
+
+ // Filter out formats that exceed maxVideoPixelsToRetain. These formats have an unnecessarily
+ // high resolution given the size at which the video will be displayed within the viewport. Also
+ // filter out formats with unknown dimensions, since we have some whose dimensions are known.
+ if (maxVideoPixelsToRetain != Integer.MAX_VALUE) {
+ for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
+ Format format = group.getFormat(selectedTrackIndices.get(i));
+ int pixelCount = format.getPixelCount();
+ if (pixelCount == Format.NO_VALUE || pixelCount > maxVideoPixelsToRetain) {
+ selectedTrackIndices.remove(i);
+ }
+ }
+ }
+
+ return selectedTrackIndices;
+ }
+
+ /**
+ * Given viewport dimensions and video dimensions, computes the maximum size of the video as it
+ * will be rendered to fit inside of the viewport.
+ */
+ private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth,
+ int viewportHeight, int videoWidth, int videoHeight) {
+ if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) {
+ // Rotation is allowed, and the video will be larger in the rotated viewport.
+ int tempViewportWidth = viewportWidth;
+ viewportWidth = viewportHeight;
+ viewportHeight = tempViewportWidth;
+ }
+
+ if (videoWidth * viewportHeight >= videoHeight * viewportWidth) {
+ // Horizontal letter-boxing along top and bottom.
+ return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth));
+ } else {
+ // Vertical letter-boxing along edges.
+ return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight);
+ }
+ }
+
+ private static final class AudioConfigurationTuple {
+
+ public final int channelCount;
+ public final int sampleRate;
+ public final String mimeType;
+
+ public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) {
+ this.channelCount = channelCount;
+ this.sampleRate = sampleRate;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ AudioConfigurationTuple other = (AudioConfigurationTuple) obj;
+ return channelCount == other.channelCount && sampleRate == other.sampleRate
+ && TextUtils.equals(mimeType, other.mimeType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = channelCount;
+ result = 31 * result + sampleRate;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ return result;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
@@ -0,0 +1,100 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link TrackSelection} consisting of a single track.
+ */
+public final class FixedTrackSelection extends BaseTrackSelection {
+
+ /**
+ * Factory for {@link FixedTrackSelection} instances.
+ */
+ public static final class Factory implements TrackSelection.Factory {
+
+ private final int reason;
+ private final Object data;
+
+ public Factory() {
+ this.reason = C.SELECTION_REASON_UNKNOWN;
+ this.data = null;
+ }
+
+ /**
+ * @param reason A reason for the track selection.
+ * @param data Optional data associated with the track selection.
+ */
+ public Factory(int reason, Object data) {
+ this.reason = reason;
+ this.data = data;
+ }
+
+ @Override
+ public FixedTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
+ Assertions.checkArgument(tracks.length == 1);
+ return new FixedTrackSelection(group, tracks[0], reason, data);
+ }
+
+ }
+
+ private final int reason;
+ private final Object data;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param track The index of the selected track within the {@link TrackGroup}.
+ */
+ public FixedTrackSelection(TrackGroup group, int track) {
+ this(group, track, C.SELECTION_REASON_UNKNOWN, null);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param track The index of the selected track within the {@link TrackGroup}.
+ * @param reason A reason for the track selection.
+ * @param data Optional data associated with the track selection.
+ */
+ public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) {
+ super(group, track);
+ this.reason = reason;
+ this.data = data;
+ }
+
+ @Override
+ public void updateSelectedTrack(long bufferedDurationUs) {
+ // Do nothing.
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return reason;
+ }
+
+ @Override
+ public Object getSelectionData() {
+ return data;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -0,0 +1,733 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.RendererConfiguration;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s
+ * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer.
+ */
+public abstract class MappingTrackSelector extends TrackSelector {
+
+ /**
+ * A track selection override.
+ */
+ public static final class SelectionOverride {
+
+ public final TrackSelection.Factory factory;
+ public final int groupIndex;
+ public final int[] tracks;
+ public final int length;
+
+ /**
+ * @param factory A factory for creating selections from this override.
+ * @param groupIndex The overriding group index.
+ * @param tracks The overriding track indices within the group.
+ */
+ public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) {
+ this.factory = factory;
+ this.groupIndex = groupIndex;
+ this.tracks = tracks;
+ this.length = tracks.length;
+ }
+
+ /**
+ * Creates an selection from this override.
+ *
+ * @param groups The groups whose selection is being overridden.
+ * @return The selection.
+ */
+ public TrackSelection createTrackSelection(TrackGroupArray groups) {
+ return factory.createTrackSelection(groups.get(groupIndex), tracks);
+ }
+
+ /**
+ * Returns whether this override contains the specified track index.
+ */
+ public boolean containsTrack(int track) {
+ for (int overrideTrack : tracks) {
+ if (overrideTrack == track) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ }
+
+ private final SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides;
+ private final SparseBooleanArray rendererDisabledFlags;
+ private int tunnelingAudioSessionId;
+
+ private MappedTrackInfo currentMappedTrackInfo;
+
+ public MappingTrackSelector() {
+ selectionOverrides = new SparseArray<>();
+ rendererDisabledFlags = new SparseBooleanArray();
+ tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ }
+
+ /**
+ * Returns the mapping information associated with the current track selections, or null if no
+ * selection is currently active.
+ */
+ public final MappedTrackInfo getCurrentMappedTrackInfo() {
+ return currentMappedTrackInfo;
+ }
+
+ /**
+ * Sets whether the renderer at the specified index is disabled.
+ *
+ * @param rendererIndex The renderer index.
+ * @param disabled Whether the renderer is disabled.
+ */
+ public final void setRendererDisabled(int rendererIndex, boolean disabled) {
+ if (rendererDisabledFlags.get(rendererIndex) == disabled) {
+ // The disabled flag is unchanged.
+ return;
+ }
+ rendererDisabledFlags.put(rendererIndex, disabled);
+ invalidate();
+ }
+
+ /**
+ * Returns whether the renderer is disabled.
+ *
+ * @param rendererIndex The renderer index.
+ * @return Whether the renderer is disabled.
+ */
+ public final boolean getRendererDisabled(int rendererIndex) {
+ return rendererDisabledFlags.get(rendererIndex);
+ }
+
+ /**
+ * Overrides the track selection for the renderer at a specified index.
+ * <p>
+ * When the {@link TrackGroupArray} available to the renderer at the specified index matches the
+ * one provided, the override is applied. When the {@link TrackGroupArray} does not match, the
+ * override has no effect. The override replaces any previous override for the renderer and the
+ * provided {@link TrackGroupArray}.
+ * <p>
+ * Passing a {@code null} override will explicitly disable the renderer. To remove overrides use
+ * {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link #clearSelectionOverrides(int)}
+ * or {@link #clearSelectionOverrides()}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groups The {@link TrackGroupArray} for which the override should be applied.
+ * @param override The override.
+ */
+ public final void setSelectionOverride(int rendererIndex, TrackGroupArray groups,
+ SelectionOverride override) {
+ Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
+ if (overrides == null) {
+ overrides = new HashMap<>();
+ selectionOverrides.put(rendererIndex, overrides);
+ }
+ if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) {
+ // The override is unchanged.
+ return;
+ }
+ overrides.put(groups, override);
+ invalidate();
+ }
+
+ /**
+ * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groups The {@link TrackGroupArray}.
+ * @return Whether there is an override.
+ */
+ public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
+ return overrides != null && overrides.containsKey(groups);
+ }
+
+ /**
+ * Returns the override for the specified renderer and {@link TrackGroupArray}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groups The {@link TrackGroupArray}.
+ * @return The override, or null if no override exists.
+ */
+ public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
+ return overrides != null ? overrides.get(groups) : null;
+ }
+
+ /**
+ * Clears a track selection override for the specified renderer and {@link TrackGroupArray}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groups The {@link TrackGroupArray} for which the override should be cleared.
+ */
+ public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
+ if (overrides == null || !overrides.containsKey(groups)) {
+ // Nothing to clear.
+ return;
+ }
+ overrides.remove(groups);
+ if (overrides.isEmpty()) {
+ selectionOverrides.remove(rendererIndex);
+ }
+ invalidate();
+ }
+
+ /**
+ * Clears all track selection override for the specified renderer.
+ *
+ * @param rendererIndex The renderer index.
+ */
+ public final void clearSelectionOverrides(int rendererIndex) {
+ Map<TrackGroupArray, ?> overrides = selectionOverrides.get(rendererIndex);
+ if (overrides == null || overrides.isEmpty()) {
+ // Nothing to clear.
+ return;
+ }
+ selectionOverrides.remove(rendererIndex);
+ invalidate();
+ }
+
+ /**
+ * Clears all track selection overrides.
+ */
+ public final void clearSelectionOverrides() {
+ if (selectionOverrides.size() == 0) {
+ // Nothing to clear.
+ return;
+ }
+ selectionOverrides.clear();
+ invalidate();
+ }
+
+ /**
+ * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in
+ * tunneling mode. Session ids can be generated using
+ * {@link C#generateAudioSessionIdV21(Context)}. To disable tunneling pass
+ * {@link C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and
+ * supported by the audio and video renderers for the selected tracks.
+ *
+ * @param tunnelingAudioSessionId The audio session id to use when tunneling, or
+ * {@link C#AUDIO_SESSION_ID_UNSET} to disable tunneling.
+ */
+ public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) {
+ if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) {
+ this.tunnelingAudioSessionId = tunnelingAudioSessionId;
+ invalidate();
+ }
+ }
+
+ // TrackSelector implementation.
+
+ @Override
+ public final TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray trackGroups) throws ExoPlaybackException {
+ // Structures into which data will be written during the selection. The extra item at the end
+ // of each array is to store data associated with track groups that cannot be associated with
+ // any renderer.
+ int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1];
+ TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][];
+ int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][];
+ for (int i = 0; i < rendererTrackGroups.length; i++) {
+ rendererTrackGroups[i] = new TrackGroup[trackGroups.length];
+ rendererFormatSupports[i] = new int[trackGroups.length][];
+ }
+
+ // Determine the extent to which each renderer supports mixed mimeType adaptation.
+ int[] mixedMimeTypeAdaptationSupport = getMixedMimeTypeAdaptationSupport(rendererCapabilities);
+
+ // Associate each track group to a preferred renderer, and evaluate the support that the
+ // renderer provides for each track in the group.
+ for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
+ TrackGroup group = trackGroups.get(groupIndex);
+ // Associate the group to a preferred renderer.
+ int rendererIndex = findRenderer(rendererCapabilities, group);
+ // Evaluate the support that the renderer provides for each track in the group.
+ int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length
+ ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group);
+ // Stash the results.
+ int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex];
+ rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group;
+ rendererFormatSupports[rendererIndex][rendererTrackGroupCount] = rendererFormatSupport;
+ rendererTrackGroupCounts[rendererIndex]++;
+ }
+
+ // Create a track group array for each renderer, and trim each rendererFormatSupports entry.
+ TrackGroupArray[] rendererTrackGroupArrays = new TrackGroupArray[rendererCapabilities.length];
+ int[] rendererTrackTypes = new int[rendererCapabilities.length];
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ int rendererTrackGroupCount = rendererTrackGroupCounts[i];
+ rendererTrackGroupArrays[i] = new TrackGroupArray(
+ Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount));
+ rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount);
+ rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();
+ }
+
+ // Create a track group array for track groups not associated with a renderer.
+ int unassociatedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];
+ TrackGroupArray unassociatedTrackGroupArray = new TrackGroupArray(Arrays.copyOf(
+ rendererTrackGroups[rendererCapabilities.length], unassociatedTrackGroupCount));
+
+ TrackSelection[] trackSelections = selectTracks(rendererCapabilities, rendererTrackGroupArrays,
+ rendererFormatSupports);
+
+ // Apply track disabling and overriding.
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ if (rendererDisabledFlags.get(i)) {
+ trackSelections[i] = null;
+ } else {
+ TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i];
+ Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(i);
+ SelectionOverride override = overrides == null ? null : overrides.get(rendererTrackGroup);
+ if (override != null) {
+ trackSelections[i] = override.createTrackSelection(rendererTrackGroup);
+ }
+ }
+ }
+
+ // Package up the track information and selections.
+ MappedTrackInfo mappedTrackInfo = new MappedTrackInfo(rendererTrackTypes,
+ rendererTrackGroupArrays, mixedMimeTypeAdaptationSupport, rendererFormatSupports,
+ unassociatedTrackGroupArray);
+
+ // Initialize the renderer configurations to the default configuration for all renderers with
+ // selections, and null otherwise.
+ RendererConfiguration[] rendererConfigurations =
+ new RendererConfiguration[rendererCapabilities.length];
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ rendererConfigurations[i] = trackSelections[i] != null ? RendererConfiguration.DEFAULT : null;
+ }
+ // Configure audio and video renderers to use tunneling if appropriate.
+ maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays,
+ rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId);
+
+ return new TrackSelectorResult(trackGroups, new TrackSelectionArray(trackSelections),
+ mappedTrackInfo, rendererConfigurations);
+ }
+
+ @Override
+ public final void onSelectionActivated(Object info) {
+ currentMappedTrackInfo = (MappedTrackInfo) info;
+ }
+
+ /**
+ * Given an array of renderers and a set of {@link TrackGroup}s mapped to each of them, provides a
+ * {@link TrackSelection} per renderer.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which
+ * {@link TrackSelection}s are to be generated.
+ * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry
+ * corresponds to the renderer of equal index in {@code renderers}.
+ * @param rendererFormatSupports Maps every available track to a specific level of support as
+ * defined by the renderer {@code FORMAT_*} constants.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports)
+ throws ExoPlaybackException;
+
+ /**
+ * Finds the renderer to which the provided {@link TrackGroup} should be associated.
+ * <p>
+ * A {@link TrackGroup} is associated to a renderer that reports
+ * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group,
+ * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In
+ * the case that two or more renderers report the same level of support, the renderer with the
+ * lowest index is associated.
+ * <p>
+ * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the
+ * tracks in the group, then {@code renderers.length} is returned to indicate that no association
+ * was made.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
+ * @param group The {@link TrackGroup} whose associated renderer is to be found.
+ * @return The index of the associated renderer, or {@code renderers.length} if no
+ * association was made.
+ * @throws ExoPlaybackException If an error occurs finding a renderer.
+ */
+ private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group)
+ throws ExoPlaybackException {
+ int bestRendererIndex = rendererCapabilities.length;
+ int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE;
+ for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) {
+ RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex];
+ for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
+ int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex))
+ & RendererCapabilities.FORMAT_SUPPORT_MASK;
+ if (formatSupportLevel > bestFormatSupportLevel) {
+ bestRendererIndex = rendererIndex;
+ bestFormatSupportLevel = formatSupportLevel;
+ if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) {
+ // We can't do better.
+ return bestRendererIndex;
+ }
+ }
+ }
+ }
+ return bestRendererIndex;
+ }
+
+ /**
+ * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified
+ * {@link TrackGroup}, returning the results in an array.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderer.
+ * @param group The {@link TrackGroup} to evaluate.
+ * @return An array containing the result of calling
+ * {@link RendererCapabilities#supportsFormat} for each track in the group.
+ * @throws ExoPlaybackException If an error occurs determining the format support.
+ */
+ private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group)
+ throws ExoPlaybackException {
+ int[] formatSupport = new int[group.length];
+ for (int i = 0; i < group.length; i++) {
+ formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i));
+ }
+ return formatSupport;
+ }
+
+ /**
+ * Calls {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer,
+ * returning the results in an array.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
+ * @return An array containing the result of calling
+ * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.
+ * @throws ExoPlaybackException If an error occurs determining the adaptation support.
+ */
+ private static int[] getMixedMimeTypeAdaptationSupport(
+ RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException {
+ int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length];
+ for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) {
+ mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation();
+ }
+ return mixedMimeTypeAdaptationSupport;
+ }
+
+ /**
+ * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in
+ * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate
+ * renderers if so.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which
+ * {@link TrackSelection}s are to be generated.
+ * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry
+ * corresponds to the renderer of equal index in {@code renderers}.
+ * @param rendererFormatSupports Maps every available track to a specific level of support as
+ * defined by the renderer {@code FORMAT_*} constants.
+ * @param rendererConfigurations The renderer configurations. Configurations may be replaced with
+ * ones that enable tunneling as a result of this call.
+ * @param trackSelections The renderer track selections.
+ * @param tunnelingAudioSessionId The audio session id to use when tunneling, or
+ * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
+ */
+ private static void maybeConfigureRenderersForTunneling(
+ RendererCapabilities[] rendererCapabilities, TrackGroupArray[] rendererTrackGroupArrays,
+ int[][][] rendererFormatSupports, RendererConfiguration[] rendererConfigurations,
+ TrackSelection[] trackSelections, int tunnelingAudioSessionId) {
+ if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+ return;
+ }
+ // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and
+ // one video renderer to support tunneling and have a selection.
+ int tunnelingAudioRendererIndex = -1;
+ int tunnelingVideoRendererIndex = -1;
+ boolean enableTunneling = true;
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ int rendererType = rendererCapabilities[i].getTrackType();
+ TrackSelection trackSelection = trackSelections[i];
+ if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO)
+ && trackSelection != null) {
+ if (rendererSupportsTunneling(rendererFormatSupports[i], rendererTrackGroupArrays[i],
+ trackSelection)) {
+ if (rendererType == C.TRACK_TYPE_AUDIO) {
+ if (tunnelingAudioRendererIndex != -1) {
+ enableTunneling = false;
+ break;
+ } else {
+ tunnelingAudioRendererIndex = i;
+ }
+ } else {
+ if (tunnelingVideoRendererIndex != -1) {
+ enableTunneling = false;
+ break;
+ } else {
+ tunnelingVideoRendererIndex = i;
+ }
+ }
+ }
+ }
+ }
+ enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1;
+ if (enableTunneling) {
+ RendererConfiguration tunnelingRendererConfiguration =
+ new RendererConfiguration(tunnelingAudioSessionId);
+ rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration;
+ rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration;
+ }
+ }
+
+ /**
+ * Returns whether a renderer supports tunneling for a {@link TrackSelection}.
+ *
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each
+ * track, indexed by group index and track index (in that order).
+ * @param trackGroups The {@link TrackGroupArray}s for the renderer.
+ * @param selection The track selection.
+ * @return Whether the renderer supports tunneling for the {@link TrackSelection}.
+ */
+ private static boolean rendererSupportsTunneling(int[][] formatSupport,
+ TrackGroupArray trackGroups, TrackSelection selection) {
+ if (selection == null) {
+ return false;
+ }
+ int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
+ for (int i = 0; i < selection.length(); i++) {
+ int trackFormatSupport = formatSupport[trackGroupIndex][selection.getIndexInTrackGroup(i)];
+ if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK)
+ != RendererCapabilities.TUNNELING_SUPPORTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Provides track information for each renderer.
+ */
+ public static final class MappedTrackInfo {
+
+ /**
+ * The renderer does not have any associated tracks.
+ */
+ public static final int RENDERER_SUPPORT_NO_TRACKS = 0;
+ /**
+ * The renderer has associated tracks, but all are of unsupported types.
+ */
+ public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1;
+ /**
+ * The renderer has associated tracks and at least one is of a supported type, but all of the
+ * tracks whose types are supported exceed the renderer's capabilities.
+ */
+ public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2;
+ /**
+ * The renderer has associated tracks and can play at least one of them.
+ */
+ public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3;
+
+ /**
+ * The number of renderers to which tracks are mapped.
+ */
+ public final int length;
+
+ private final int[] rendererTrackTypes;
+ private final TrackGroupArray[] trackGroups;
+ private final int[] mixedMimeTypeAdaptiveSupport;
+ private final int[][][] formatSupport;
+ private final TrackGroupArray unassociatedTrackGroups;
+
+ /**
+ * @param rendererTrackTypes The track type supported by each renderer.
+ * @param trackGroups The {@link TrackGroupArray}s for each renderer.
+ * @param mixedMimeTypeAdaptiveSupport The result of
+ * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.
+ * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each
+ * track, indexed by renderer index, group index and track index (in that order).
+ * @param unassociatedTrackGroups Contains {@link TrackGroup}s not associated with any renderer.
+ */
+ /* package */ MappedTrackInfo(int[] rendererTrackTypes,
+ TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport,
+ int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) {
+ this.rendererTrackTypes = rendererTrackTypes;
+ this.trackGroups = trackGroups;
+ this.formatSupport = formatSupport;
+ this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport;
+ this.unassociatedTrackGroups = unassociatedTrackGroups;
+ this.length = trackGroups.length;
+ }
+
+ /**
+ * Returns the array of {@link TrackGroup}s associated to the renderer at a specified index.
+ *
+ * @param rendererIndex The renderer index.
+ * @return The corresponding {@link TrackGroup}s.
+ */
+ public TrackGroupArray getTrackGroups(int rendererIndex) {
+ return trackGroups[rendererIndex];
+ }
+
+ /**
+ * Returns the extent to which a renderer can support playback of the tracks associated to it.
+ *
+ * @param rendererIndex The renderer index.
+ * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
+ * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
+ * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
+ */
+ public int getRendererSupport(int rendererIndex) {
+ int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+ int[][] rendererFormatSupport = formatSupport[rendererIndex];
+ for (int i = 0; i < rendererFormatSupport.length; i++) {
+ for (int j = 0; j < rendererFormatSupport[i].length; j++) {
+ int trackRendererSupport;
+ switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) {
+ case RendererCapabilities.FORMAT_HANDLED:
+ return RENDERER_SUPPORT_PLAYABLE_TRACKS;
+ case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
+ trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS;
+ break;
+ default:
+ trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;
+ break;
+ }
+ bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);
+ }
+ }
+ return bestRendererSupport;
+ }
+
+ /**
+ * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all
+ * renderers of the specified track type. If no renderers exist for the specified type then
+ * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned.
+ *
+ * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
+ * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
+ * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
+ */
+ public int getTrackTypeRendererSupport(int trackType) {
+ int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+ for (int i = 0; i < length; i++) {
+ if (rendererTrackTypes[i] == trackType) {
+ bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i));
+ }
+ }
+ return bestRendererSupport;
+ }
+
+ /**
+ * Returns the extent to which the format of an individual track is supported by the renderer.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the group to which the track belongs.
+ * @param trackIndex The index of the track within the group.
+ * @return One of {@link RendererCapabilities#FORMAT_HANDLED},
+ * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}.
+ */
+ public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) {
+ return formatSupport[rendererIndex][groupIndex][trackIndex]
+ & RendererCapabilities.FORMAT_SUPPORT_MASK;
+ }
+
+ /**
+ * Returns the extent to which the renderer supports adaptation between supported tracks in a
+ * specified {@link TrackGroup}.
+ * <p>
+ * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_HANDLED} are always considered.
+ * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.
+ * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+ * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if
+ * {@code includeCapabilitiesExceededTracks} is set to {@code true}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the group.
+ * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the
+ * renderer should be included when determining support. False otherwise.
+ * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
+ */
+ public int getAdaptiveSupport(int rendererIndex, int groupIndex,
+ boolean includeCapabilitiesExceededTracks) {
+ int trackCount = trackGroups[rendererIndex].get(groupIndex).length;
+ // Iterate over the tracks in the group, recording the indices of those to consider.
+ int[] trackIndices = new int[trackCount];
+ int trackIndexCount = 0;
+ for (int i = 0; i < trackCount; i++) {
+ int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i);
+ if (fixedSupport == RendererCapabilities.FORMAT_HANDLED
+ || (includeCapabilitiesExceededTracks
+ && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) {
+ trackIndices[trackIndexCount++] = i;
+ }
+ }
+ trackIndices = Arrays.copyOf(trackIndices, trackIndexCount);
+ return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices);
+ }
+
+ /**
+ * Returns the extent to which the renderer supports adaptation between specified tracks within
+ * a {@link TrackGroup}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the group.
+ * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
+ * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
+ */
+ public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {
+ int handledTrackCount = 0;
+ int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS;
+ boolean multipleMimeTypes = false;
+ String firstSampleMimeType = null;
+ for (int i = 0; i < trackIndices.length; i++) {
+ int trackIndex = trackIndices[i];
+ String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex)
+ .sampleMimeType;
+ if (handledTrackCount++ == 0) {
+ firstSampleMimeType = sampleMimeType;
+ } else {
+ multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType);
+ }
+ adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i]
+ & RendererCapabilities.ADAPTIVE_SUPPORT_MASK);
+ }
+ return multipleMimeTypes
+ ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex])
+ : adaptiveSupport;
+ }
+
+ /**
+ * Returns the {@link TrackGroup}s not associated with any renderer.
+ */
+ public TrackGroupArray getUnassociatedTrackGroups() {
+ return unassociatedTrackGroups;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.TrackGroup;
+import java.util.Random;
+
+/**
+ * A {@link TrackSelection} whose selected track is updated randomly.
+ */
+public final class RandomTrackSelection extends BaseTrackSelection {
+
+ /**
+ * Factory for {@link RandomTrackSelection} instances.
+ */
+ public static final class Factory implements TrackSelection.Factory {
+
+ private final Random random;
+
+ public Factory() {
+ random = new Random();
+ }
+
+ /**
+ * @param seed A seed for the {@link Random} instance used by the factory.
+ */
+ public Factory(int seed) {
+ random = new Random(seed);
+ }
+
+ @Override
+ public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
+ return new RandomTrackSelection(group, tracks, random);
+ }
+
+ }
+
+ private final Random random;
+
+ private int selectedIndex;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ */
+ public RandomTrackSelection(TrackGroup group, int... tracks) {
+ super(group, tracks);
+ random = new Random();
+ selectedIndex = random.nextInt(length);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ * @param seed A seed for the {@link Random} instance used to update the selected track.
+ */
+ public RandomTrackSelection(TrackGroup group, int[] tracks, long seed) {
+ this(group, tracks, new Random(seed));
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ * @param random A source of random numbers.
+ */
+ public RandomTrackSelection(TrackGroup group, int[] tracks, Random random) {
+ super(group, tracks);
+ this.random = random;
+ selectedIndex = random.nextInt(length);
+ }
+
+ @Override
+ public void updateSelectedTrack(long bufferedDurationUs) {
+ // Count the number of non-blacklisted formats.
+ long nowMs = SystemClock.elapsedRealtime();
+ int nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (!isBlacklisted(i, nowMs)) {
+ nonBlacklistedFormatCount++;
+ }
+ }
+
+ selectedIndex = random.nextInt(nonBlacklistedFormatCount);
+ if (nonBlacklistedFormatCount != length) {
+ // Adjust the format index to account for blacklisted formats.
+ nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (!isBlacklisted(i, nowMs) && selectedIndex == nonBlacklistedFormatCount++) {
+ selectedIndex = i;
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_ADAPTIVE;
+ }
+
+ @Override
+ public Object getSelectionData() {
+ return null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -0,0 +1,163 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import java.util.List;
+
+/**
+ * A track selection consisting of a static subset of selected tracks belonging to a
+ * {@link TrackGroup}, and a possibly varying individual selected track from the subset.
+ * <p>
+ * Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected
+ * track may change as a result of calling {@link #updateSelectedTrack(long)}.
+ */
+public interface TrackSelection {
+
+ /**
+ * Factory for {@link TrackSelection} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates a new selection.
+ *
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ * @return The created selection.
+ */
+ TrackSelection createTrackSelection(TrackGroup group, int... tracks);
+
+ }
+
+ /**
+ * Returns the {@link TrackGroup} to which the selected tracks belong.
+ */
+ TrackGroup getTrackGroup();
+
+ // Static subset of selected tracks.
+
+ /**
+ * Returns the number of tracks in the selection.
+ */
+ int length();
+
+ /**
+ * Returns the format of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The format of the selected track.
+ */
+ Format getFormat(int index);
+
+ /**
+ * Returns the index in the track group of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The index of the selected track.
+ */
+ int getIndexInTrackGroup(int index);
+
+ /**
+ * Returns the index in the selection of the track with the specified format.
+ *
+ * @param format The format.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * format is not part of the selection.
+ */
+ int indexOf(Format format);
+
+ /**
+ * Returns the index in the selection of the track with the specified index in the track group.
+ *
+ * @param indexInTrackGroup The index in the track group.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * index is not part of the selection.
+ */
+ int indexOf(int indexInTrackGroup);
+
+ // Individual selected track.
+
+ /**
+ * Returns the {@link Format} of the individual selected track.
+ */
+ Format getSelectedFormat();
+
+ /**
+ * Returns the index in the track group of the individual selected track.
+ */
+ int getSelectedIndexInTrackGroup();
+
+ /**
+ * Returns the index of the selected track.
+ */
+ int getSelectedIndex();
+
+ /**
+ * Returns the reason for the current track selection.
+ */
+ int getSelectionReason();
+
+ /**
+ * Returns optional data associated with the current track selection.
+ */
+ Object getSelectionData();
+
+ // Adaptation.
+
+ /**
+ * Updates the selected track.
+ *
+ * @param bufferedDurationUs The duration of media currently buffered in microseconds.
+ */
+ void updateSelectedTrack(long bufferedDurationUs);
+
+ /**
+ * May be called periodically by sources that load media in discrete {@link MediaChunk}s and
+ * support discarding of buffered chunks in order to re-buffer using a different selected track.
+ * Returns the number of chunks that should be retained in the queue.
+ * <p>
+ * To avoid excessive re-buffering, implementations should normally return the size of the queue.
+ * An example of a case where a smaller value may be returned is if network conditions have
+ * improved dramatically, allowing chunks to be discarded and re-buffered in a track of
+ * significantly higher quality. Discarding chunks may allow faster switching to a higher quality
+ * track in this case.
+ *
+ * @param playbackPositionUs The current playback position in microseconds.
+ * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified.
+ * @return The number of chunks to retain in the queue.
+ */
+ int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
+
+ /**
+ * Attempts to blacklist the track at the specified index in the selection, making it ineligible
+ * for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time.
+ * Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the
+ * currently selected track, note that it will remain selected until the next call to
+ * {@link #updateSelectedTrack(long)}.
+ *
+ * @param index The index of the track in the selection.
+ * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in
+ * milliseconds.
+ * @return Whether blacklisting was successful.
+ */
+ boolean blacklist(int index, long blacklistDurationMs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import java.util.Arrays;
+
+/**
+ * The result of a {@link TrackSelector} operation.
+ */
+public final class TrackSelectionArray {
+
+ /**
+ * The number of selections in the result. Greater than or equal to zero.
+ */
+ public final int length;
+
+ private final TrackSelection[] trackSelections;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param trackSelections The selections. Must not be null, but may contain null elements.
+ */
+ public TrackSelectionArray(TrackSelection... trackSelections) {
+ this.trackSelections = trackSelections;
+ this.length = trackSelections.length;
+ }
+
+ /**
+ * Returns the selection at a given index.
+ *
+ * @param index The index of the selection.
+ * @return The selection.
+ */
+ public TrackSelection get(int index) {
+ return trackSelections[index];
+ }
+
+ /**
+ * Returns the selections in a newly allocated array.
+ */
+ public TrackSelection[] getAll() {
+ return trackSelections.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + Arrays.hashCode(trackSelections);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackSelectionArray other = (TrackSelectionArray) obj;
+ return Arrays.equals(trackSelections, other.trackSelections);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+
+/** Selects tracks to be consumed by available renderers. */
+public abstract class TrackSelector {
+
+ /**
+ * Notified when previous selections by a {@link TrackSelector} are no longer valid.
+ */
+ public interface InvalidationListener {
+
+ /**
+ * Called by a {@link TrackSelector} when previous selections are no longer valid.
+ */
+ void onTrackSelectionsInvalidated();
+
+ }
+
+ private InvalidationListener listener;
+
+ /**
+ * Initializes the selector.
+ *
+ * @param listener A listener for the selector.
+ */
+ public final void init(InvalidationListener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Performs a track selection for renderers.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
+ * are to be selected.
+ * @param trackGroups The available track groups.
+ * @return A {@link TrackSelectorResult} describing the track selections.
+ * @throws ExoPlaybackException If an error occurs selecting tracks.
+ */
+ public abstract TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray trackGroups) throws ExoPlaybackException;
+
+ /**
+ * Called when a {@link TrackSelectorResult} previously generated by
+ * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated.
+ *
+ * @param info The value of {@link TrackSelectorResult#info} in the activated result.
+ */
+ public abstract void onSelectionActivated(Object info);
+
+ /**
+ * Invalidates all previously generated track selections.
+ */
+ protected final void invalidate() {
+ if (listener != null) {
+ listener.onTrackSelectionsInvalidated();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.RendererConfiguration;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * The result of a {@link TrackSelector} operation.
+ */
+public final class TrackSelectorResult {
+
+ /**
+ * The groups provided to the {@link TrackSelector}.
+ */
+ public final TrackGroupArray groups;
+ /**
+ * A {@link TrackSelectionArray} containing the selection for each renderer.
+ */
+ public final TrackSelectionArray selections;
+ /**
+ * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
+ * should the selections be activated.
+ */
+ public final Object info;
+ /**
+ * A {@link RendererConfiguration} for each renderer, to be used with the selections.
+ */
+ public final RendererConfiguration[] rendererConfigurations;
+
+ /**
+ * @param groups The groups provided to the {@link TrackSelector}.
+ * @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
+ * @param info An opaque object that will be returned to
+ * {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated.
+ * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used
+ * with the selections.
+ */
+ public TrackSelectorResult(TrackGroupArray groups, TrackSelectionArray selections, Object info,
+ RendererConfiguration[] rendererConfigurations) {
+ this.groups = groups;
+ this.selections = selections;
+ this.info = info;
+ this.rendererConfigurations = rendererConfigurations;
+ }
+
+ /**
+ * Returns whether this result is equivalent to {@code other} for all renderers.
+ *
+ * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
+ * will be returned in all cases.
+ * @return Whether this result is equivalent to {@code other} for all renderers.
+ */
+ public boolean isEquivalent(TrackSelectorResult other) {
+ if (other == null) {
+ return false;
+ }
+ for (int i = 0; i < selections.length; i++) {
+ if (!isEquivalent(other, i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether this result is equivalent to {@code other} for the renderer at the given index.
+ * The results are equivalent if they have equal track selections and configurations for the
+ * renderer.
+ *
+ * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
+ * will be returned in all cases.
+ * @param index The renderer index to check for equivalence.
+ * @return Whether this result is equivalent to {@code other} for all renderers.
+ */
+ public boolean isEquivalent(TrackSelectorResult other, int index) {
+ if (other == null) {
+ return false;
+ }
+ return Util.areEqual(selections.get(index), other.selections.get(index))
+ && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/Allocation.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+/**
+ * An allocation within a byte array.
+ * <p>
+ * The allocation's length is obtained by calling {@link Allocator#getIndividualAllocationLength()}
+ * on the {@link Allocator} from which it was obtained.
+ */
+public final class Allocation {
+
+ /**
+ * The array containing the allocated space. The allocated space might not be at the start of the
+ * array, and so {@link #translateOffset(int)} method must be used when indexing into it.
+ */
+ public final byte[] data;
+
+ private final int offset;
+
+ /**
+ * @param data The array containing the allocated space.
+ * @param offset The offset of the allocated space within the array.
+ */
+ public Allocation(byte[] data, int offset) {
+ this.data = data;
+ this.offset = offset;
+ }
+
+ /**
+ * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset.
+ *
+ * @param offset The zero-based offset to translate.
+ * @return The corresponding offset in {@link #data}.
+ */
+ public int translateOffset(int offset) {
+ return this.offset + offset;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/Allocator.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+/**
+ * A source of allocations.
+ */
+public interface Allocator {
+
+ /**
+ * Obtain an {@link Allocation}.
+ * <p>
+ * When the caller has finished with the {@link Allocation}, it should be returned by calling
+ * {@link #release(Allocation)}.
+ *
+ * @return The {@link Allocation}.
+ */
+ Allocation allocate();
+
+ /**
+ * Releases an {@link Allocation} back to the allocator.
+ *
+ * @param allocation The {@link Allocation} being released.
+ */
+ void release(Allocation allocation);
+
+ /**
+ * Releases an array of {@link Allocation}s back to the allocator.
+ *
+ * @param allocations The array of {@link Allocation}s being released.
+ */
+ void release(Allocation[] allocations);
+
+ /**
+ * Hints to the allocator that it should make a best effort to release any excess
+ * {@link Allocation}s.
+ */
+ void trim();
+
+ /**
+ * Returns the total number of bytes currently allocated.
+ */
+ int getTotalBytesAllocated();
+
+ /**
+ * Returns the length of each individual {@link Allocation}.
+ */
+ int getIndividualAllocationLength();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/AssetDataSource.java
@@ -0,0 +1,163 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading from a local asset.
+ */
+public final class AssetDataSource implements DataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading a local asset.
+ */
+ public static final class AssetDataSourceException extends IOException {
+
+ public AssetDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final AssetManager assetManager;
+ private final TransferListener<? super AssetDataSource> listener;
+
+ private Uri uri;
+ private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public AssetDataSource(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * @param context A context.
+ * @param listener An optional listener.
+ */
+ public AssetDataSource(Context context, TransferListener<? super AssetDataSource> listener) {
+ this.assetManager = context.getAssets();
+ this.listener = listener;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws AssetDataSourceException {
+ try {
+ uri = dataSpec.uri;
+ String path = uri.getPath();
+ if (path.startsWith("/android_asset/")) {
+ path = path.substring(15);
+ } else if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM);
+ long skipped = inputStream.skip(dataSpec.position);
+ if (skipped < dataSpec.position) {
+ // assetManager.open() returns an AssetInputStream, whose skip() implementation only skips
+ // fewer bytes than requested if the skip is beyond the end of the asset's data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ bytesRemaining = inputStream.available();
+ if (bytesRemaining == Integer.MAX_VALUE) {
+ // assetManager.open() returns an AssetInputStream, whose available() implementation
+ // returns Integer.MAX_VALUE if the remaining length is greater than (or equal to)
+ // Integer.MAX_VALUE. We don't know the true length in this case, so treat as unbounded.
+ bytesRemaining = C.LENGTH_UNSET;
+ }
+ }
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = inputStream.read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new AssetDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ if (listener != null) {
+ listener.onBytesTransferred(this, bytesRead);
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws AssetDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new AssetDataSourceException(e);
+ } finally {
+ inputStream = null;
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+/**
+ * Provides estimates of the currently available bandwidth.
+ */
+public interface BandwidthMeter {
+
+ /**
+ * A listener of {@link BandwidthMeter} events.
+ */
+ interface EventListener {
+
+ /**
+ * Called periodically to indicate that bytes have been transferred.
+ * <p>
+ * Note: The estimated bitrate is typically derived from more information than just
+ * {@code bytes} and {@code elapsedMs}.
+ *
+ * @param elapsedMs The time taken to transfer the bytes, in milliseconds.
+ * @param bytes The number of bytes transferred.
+ * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if an estimate is
+ * not available.
+ */
+ void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
+
+ }
+
+ /**
+ * Indicates no bandwidth estimate is available.
+ */
+ long NO_ESTIMATE = -1;
+
+ /**
+ * Returns the estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if an estimate is not
+ * available.
+ */
+ long getBitrateEstimate();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * A {@link DataSink} for writing to a byte array.
+ */
+public final class ByteArrayDataSink implements DataSink {
+
+ private ByteArrayOutputStream stream;
+
+ @Override
+ public void open(DataSpec dataSpec) throws IOException {
+ if (dataSpec.length == C.LENGTH_UNSET) {
+ stream = new ByteArrayOutputStream();
+ } else {
+ Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
+ stream = new ByteArrayOutputStream((int) dataSpec.length);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws IOException {
+ stream.write(buffer, offset, length);
+ }
+
+ /**
+ * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if
+ * {@link #open(DataSpec)} has never been called.
+ */
+ public byte[] getData() {
+ return stream == null ? null : stream.toByteArray();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java
@@ -0,0 +1,81 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * A {@link DataSource} for reading from a byte array.
+ */
+public final class ByteArrayDataSource implements DataSource {
+
+ private final byte[] data;
+
+ private Uri uri;
+ private int readPosition;
+ private int bytesRemaining;
+
+ /**
+ * @param data The data to be read.
+ */
+ public ByteArrayDataSource(byte[] data) {
+ Assertions.checkNotNull(data);
+ Assertions.checkArgument(data.length > 0);
+ this.data = data;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ uri = dataSpec.uri;
+ readPosition = (int) dataSpec.position;
+ bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET)
+ ? (data.length - dataSpec.position) : dataSpec.length);
+ if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) {
+ throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length
+ + "], length: " + data.length);
+ }
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ readLength = Math.min(readLength, bytesRemaining);
+ System.arraycopy(data, readPosition, buffer, offset, readLength);
+ readPosition += readLength;
+ bytesRemaining -= readLength;
+ return readLength;
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws IOException {
+ uri = null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/ContentDataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading from a content URI.
+ */
+public final class ContentDataSource implements DataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading from a content URI.
+ */
+ public static class ContentDataSourceException extends IOException {
+
+ public ContentDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final ContentResolver resolver;
+ private final TransferListener<? super ContentDataSource> listener;
+
+ private Uri uri;
+ private AssetFileDescriptor assetFileDescriptor;
+ private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public ContentDataSource(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * @param context A context.
+ * @param listener An optional listener.
+ */
+ public ContentDataSource(Context context, TransferListener<? super ContentDataSource> listener) {
+ this.resolver = context.getContentResolver();
+ this.listener = listener;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws ContentDataSourceException {
+ try {
+ uri = dataSpec.uri;
+ assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r");
+ inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
+ long skipped = inputStream.skip(dataSpec.position);
+ if (skipped < dataSpec.position) {
+ // We expect the skip to be satisfied in full. If it isn't then we're probably trying to
+ // skip beyond the end of the data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ bytesRemaining = inputStream.available();
+ if (bytesRemaining == 0) {
+ // FileInputStream.available() returns 0 if the remaining length cannot be determined, or
+ // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case,
+ // so treat as unbounded.
+ bytesRemaining = C.LENGTH_UNSET;
+ }
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = inputStream.read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new ContentDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ if (listener != null) {
+ listener.onBytesTransferred(this, bytesRead);
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws ContentDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ } finally {
+ inputStream = null;
+ try {
+ if (assetFileDescriptor != null) {
+ assetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ throw new ContentDataSourceException(e);
+ } finally {
+ assetFileDescriptor = null;
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DataSink.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import java.io.IOException;
+
+/**
+ * A component to which streams of data can be written.
+ */
+public interface DataSink {
+
+ /**
+ * A factory for {@link DataSink} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates a {@link DataSink} instance.
+ */
+ DataSink createDataSink();
+
+ }
+
+ /**
+ * Opens the sink to consume the specified data.
+ *
+ * @param dataSpec Defines the data to be consumed.
+ * @throws IOException If an error occurs opening the sink.
+ */
+ void open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Consumes the provided data.
+ *
+ * @param buffer The buffer from which data should be consumed.
+ * @param offset The offset of the data to consume in {@code buffer}.
+ * @param length The length of the data to consume, in bytes.
+ * @throws IOException If an error occurs writing to the sink.
+ */
+ void write(byte[] buffer, int offset, int length) throws IOException;
+
+ /**
+ * Closes the sink.
+ *
+ * @throws IOException If an error occurs closing the sink.
+ */
+ void close() throws IOException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.IOException;
+
+/**
+ * A component from which streams of data can be read.
+ */
+public interface DataSource {
+
+ /**
+ * A factory for {@link DataSource} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates a {@link DataSource} instance.
+ */
+ DataSource createDataSource();
+
+ }
+
+ /**
+ * Opens the source to read the specified data.
+ * <p>
+ * Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to ensure
+ * that any partial effects of the invocation are cleaned up.
+ *
+ * @param dataSpec Defines the data to be read.
+ * @throws IOException If an error occurs opening the source. {@link DataSourceException} can be
+ * thrown or used as a cause of the thrown exception to specify the reason of the error.
+ * @return The number of bytes that can be read from the opened source. For unbounded requests
+ * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNSET}) this value
+ * is the resolved length of the request, or {@link C#LENGTH_UNSET} if the length is still
+ * unresolved. For all other requests, the value returned will be equal to the request's
+ * {@link DataSpec#length}.
+ */
+ long open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ * <p>
+ * If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the
+ * end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.
+ * Otherwise, the call will block until at least one byte of data has been read and the number of
+ * bytes read is returned.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
+ * because the end of the opened range has been reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ int read(byte[] buffer, int offset, int readLength) throws IOException;
+
+ /**
+ * When the source is open, returns the {@link Uri} from which data is being read. The returned
+ * {@link Uri} will be identical to the one passed {@link #open(DataSpec)} in the {@link DataSpec}
+ * unless redirection has occurred. If redirection has occurred, the {@link Uri} after redirection
+ * is returned.
+ *
+ * @return The {@link Uri} from which data is being read, or null if the source is not open.
+ */
+ Uri getUri();
+
+ /**
+ * Closes the source.
+ * <p>
+ * Note: This method must be called even if the corresponding call to {@link #open(DataSpec)}
+ * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.
+ *
+ * @throws IOException If an error occurs closing the source.
+ */
+ void close() throws IOException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DataSourceException.java
@@ -0,0 +1,41 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import java.io.IOException;
+
+/**
+ * Used to specify reason of a DataSource error.
+ */
+public final class DataSourceException extends IOException {
+
+ public static final int POSITION_OUT_OF_RANGE = 0;
+
+ /**
+ * The reason of this {@link DataSourceException}. It can only be {@link #POSITION_OUT_OF_RANGE}.
+ */
+ public final int reason;
+
+ /**
+ * Constructs a DataSourceException.
+ *
+ * @param reason Reason of the error. It can only be {@link #POSITION_OUT_OF_RANGE}.
+ */
+ public DataSourceException(int reason) {
+ this.reason = reason;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DataSourceInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and
+ * consumed through an {@link InputStream}.
+ */
+public final class DataSourceInputStream extends InputStream {
+
+ private final DataSource dataSource;
+ private final DataSpec dataSpec;
+ private final byte[] singleByteArray;
+
+ private boolean opened = false;
+ private boolean closed = false;
+ private long totalBytesRead;
+
+ /**
+ * @param dataSource The {@link DataSource} from which the data should be read.
+ * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}.
+ */
+ public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) {
+ this.dataSource = dataSource;
+ this.dataSpec = dataSpec;
+ singleByteArray = new byte[1];
+ }
+
+ /**
+ * Returns the total number of bytes that have been read or skipped.
+ */
+ public long bytesRead() {
+ return totalBytesRead;
+ }
+
+ /**
+ * Optional call to open the underlying {@link DataSource}.
+ * <p>
+ * Calling this method does nothing if the {@link DataSource} is already open. Calling this
+ * method is optional, since the read and skip methods will automatically open the underlying
+ * {@link DataSource} if it's not open already.
+ *
+ * @throws IOException If an error occurs opening the {@link DataSource}.
+ */
+ public void open() throws IOException {
+ checkOpened();
+ }
+
+ @Override
+ public int read() throws IOException {
+ int length = read(singleByteArray);
+ return length == -1 ? -1 : (singleByteArray[0] & 0xFF);
+ }
+
+ @Override
+ public int read(@NonNull byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
+ Assertions.checkState(!closed);
+ checkOpened();
+ int bytesRead = dataSource.read(buffer, offset, length);
+ if (bytesRead == C.RESULT_END_OF_INPUT) {
+ return -1;
+ } else {
+ totalBytesRead += bytesRead;
+ return bytesRead;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ dataSource.close();
+ closed = true;
+ }
+ }
+
+ private void checkOpened() throws IOException {
+ if (!opened) {
+ dataSource.open(dataSpec);
+ opened = true;
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -0,0 +1,190 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * Defines a region of data.
+ */
+public final class DataSpec {
+
+ /**
+ * The flags that apply to any request for data.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH})
+ public @interface Flags {}
+ /**
+ * Permits an underlying network stack to request that the server use gzip compression.
+ * <p>
+ * Should not typically be set if the data being requested is already compressed (e.g. most audio
+ * and video requests). May be set when requesting other data.
+ * <p>
+ * When a {@link DataSource} is used to request data with this flag set, and if the
+ * {@link DataSource} does make a network request, then the value returned from
+ * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from
+ * {@link DataSource#read(byte[], int, int)} will be the decompressed data.
+ */
+ public static final int FLAG_ALLOW_GZIP = 1 << 0;
+
+ /**
+ * Permits content to be cached even if its length can not be resolved.
+ */
+ public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1;
+
+ /**
+ * The source from which data should be read.
+ */
+ public final Uri uri;
+ /**
+ * Body for a POST request, null otherwise.
+ */
+ public final byte[] postBody;
+ /**
+ * The absolute position of the data in the full stream.
+ */
+ public final long absoluteStreamPosition;
+ /**
+ * The position of the data when read from {@link #uri}.
+ * <p>
+ * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location
+ * of a subset of the underyling data.
+ */
+ public final long position;
+ /**
+ * The length of the data, or {@link C#LENGTH_UNSET}.
+ */
+ public final long length;
+ /**
+ * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
+ * {@link DataSpec} is not intended to be used in conjunction with a cache.
+ */
+ public final String key;
+ /**
+ * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
+ * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
+ */
+ @Flags public final int flags;
+
+ /**
+ * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
+ *
+ * @param uri {@link #uri}.
+ */
+ public DataSpec(Uri uri) {
+ this(uri, 0);
+ }
+
+ /**
+ * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
+ *
+ * @param uri {@link #uri}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(Uri uri, @Flags int flags) {
+ this(uri, 0, C.LENGTH_UNSET, null, flags);
+ }
+
+ /**
+ * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) {
+ this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0);
+ }
+
+ /**
+ * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) {
+ this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
+ }
+
+ /**
+ * Construct a {@link DataSpec} where {@link #position} may differ from
+ * {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key,
+ @Flags int flags) {
+ this(uri, null, absoluteStreamPosition, position, length, key, flags);
+ }
+
+ /**
+ * Construct a {@link DataSpec} where {@link #position} may differ from
+ * {@link #absoluteStreamPosition}.
+ *
+ * @param uri {@link #uri}.
+ * @param postBody {@link #postBody}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param position {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param flags {@link #flags}.
+ */
+ public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length,
+ String key, @Flags int flags) {
+ Assertions.checkArgument(absoluteStreamPosition >= 0);
+ Assertions.checkArgument(position >= 0);
+ Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);
+ this.uri = uri;
+ this.postBody = postBody;
+ this.absoluteStreamPosition = absoluteStreamPosition;
+ this.position = position;
+ this.length = length;
+ this.key = key;
+ this.flags = flags;
+ }
+
+ /**
+ * Returns whether the given flag is set.
+ *
+ * @param flag Flag to be checked if it is set.
+ */
+ public boolean isFlagSet(@Flags int flag) {
+ return (this.flags & flag) == flag;
+ }
+
+ @Override
+ public String toString() {
+ return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition
+ + ", " + position + ", " + length + ", " + key + ", " + flags + "]";
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java
@@ -0,0 +1,182 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Default implementation of {@link Allocator}.
+ */
+public final class DefaultAllocator implements Allocator {
+
+ private static final int AVAILABLE_EXTRA_CAPACITY = 100;
+
+ private final boolean trimOnReset;
+ private final int individualAllocationSize;
+ private final byte[] initialAllocationBlock;
+ private final Allocation[] singleAllocationReleaseHolder;
+
+ private int targetBufferSize;
+ private int allocatedCount;
+ private int availableCount;
+ private Allocation[] availableAllocations;
+
+ /**
+ * Constructs an instance without creating any {@link Allocation}s up front.
+ *
+ * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
+ * the allocator will be re-used by multiple player instances.
+ * @param individualAllocationSize The length of each individual {@link Allocation}.
+ */
+ public DefaultAllocator(boolean trimOnReset, int individualAllocationSize) {
+ this(trimOnReset, individualAllocationSize, 0);
+ }
+
+ /**
+ * Constructs an instance with some {@link Allocation}s created up front.
+ * <p>
+ * Note: {@link Allocation}s created up front will never be discarded by {@link #trim()}.
+ *
+ * @param trimOnReset Whether memory is freed when the allocator is reset. Should be true unless
+ * the allocator will be re-used by multiple player instances.
+ * @param individualAllocationSize The length of each individual {@link Allocation}.
+ * @param initialAllocationCount The number of allocations to create up front.
+ */
+ public DefaultAllocator(boolean trimOnReset, int individualAllocationSize,
+ int initialAllocationCount) {
+ Assertions.checkArgument(individualAllocationSize > 0);
+ Assertions.checkArgument(initialAllocationCount >= 0);
+ this.trimOnReset = trimOnReset;
+ this.individualAllocationSize = individualAllocationSize;
+ this.availableCount = initialAllocationCount;
+ this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY];
+ if (initialAllocationCount > 0) {
+ initialAllocationBlock = new byte[initialAllocationCount * individualAllocationSize];
+ for (int i = 0; i < initialAllocationCount; i++) {
+ int allocationOffset = i * individualAllocationSize;
+ availableAllocations[i] = new Allocation(initialAllocationBlock, allocationOffset);
+ }
+ } else {
+ initialAllocationBlock = null;
+ }
+ singleAllocationReleaseHolder = new Allocation[1];
+ }
+
+ public synchronized void reset() {
+ if (trimOnReset) {
+ setTargetBufferSize(0);
+ }
+ }
+
+ public synchronized void setTargetBufferSize(int targetBufferSize) {
+ boolean targetBufferSizeReduced = targetBufferSize < this.targetBufferSize;
+ this.targetBufferSize = targetBufferSize;
+ if (targetBufferSizeReduced) {
+ trim();
+ }
+ }
+
+ @Override
+ public synchronized Allocation allocate() {
+ allocatedCount++;
+ Allocation allocation;
+ if (availableCount > 0) {
+ allocation = availableAllocations[--availableCount];
+ availableAllocations[availableCount] = null;
+ } else {
+ allocation = new Allocation(new byte[individualAllocationSize], 0);
+ }
+ return allocation;
+ }
+
+ @Override
+ public synchronized void release(Allocation allocation) {
+ singleAllocationReleaseHolder[0] = allocation;
+ release(singleAllocationReleaseHolder);
+ }
+
+ @Override
+ public synchronized void release(Allocation[] allocations) {
+ if (availableCount + allocations.length >= availableAllocations.length) {
+ availableAllocations = Arrays.copyOf(availableAllocations,
+ Math.max(availableAllocations.length * 2, availableCount + allocations.length));
+ }
+ for (Allocation allocation : allocations) {
+ // Weak sanity check that the allocation probably originated from this pool.
+ Assertions.checkArgument(allocation.data == initialAllocationBlock
+ || allocation.data.length == individualAllocationSize);
+ availableAllocations[availableCount++] = allocation;
+ }
+ allocatedCount -= allocations.length;
+ // Wake up threads waiting for the allocated size to drop.
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void trim() {
+ int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize);
+ int targetAvailableCount = Math.max(0, targetAllocationCount - allocatedCount);
+ if (targetAvailableCount >= availableCount) {
+ // We're already at or below the target.
+ return;
+ }
+
+ if (initialAllocationBlock != null) {
+ // Some allocations are backed by an initial block. We need to make sure that we hold onto all
+ // such allocations. Re-order the available allocations so that the ones backed by the initial
+ // block come first.
+ int lowIndex = 0;
+ int highIndex = availableCount - 1;
+ while (lowIndex <= highIndex) {
+ Allocation lowAllocation = availableAllocations[lowIndex];
+ if (lowAllocation.data == initialAllocationBlock) {
+ lowIndex++;
+ } else {
+ Allocation highAllocation = availableAllocations[highIndex];
+ if (highAllocation.data != initialAllocationBlock) {
+ highIndex--;
+ } else {
+ availableAllocations[lowIndex++] = highAllocation;
+ availableAllocations[highIndex--] = lowAllocation;
+ }
+ }
+ }
+ // lowIndex is the index of the first allocation not backed by an initial block.
+ targetAvailableCount = Math.max(targetAvailableCount, lowIndex);
+ if (targetAvailableCount >= availableCount) {
+ // We're already at or below the target.
+ return;
+ }
+ }
+
+ // Discard allocations beyond the target.
+ Arrays.fill(availableAllocations, targetAvailableCount, availableCount, null);
+ availableCount = targetAvailableCount;
+ }
+
+ @Override
+ public synchronized int getTotalBytesAllocated() {
+ return allocatedCount * individualAllocationSize;
+ }
+
+ @Override
+ public int getIndividualAllocationLength() {
+ return individualAllocationSize;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
@@ -0,0 +1,117 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.SlidingPercentile;
+
+/**
+ * Estimates bandwidth by listening to data transfers. The bandwidth estimate is calculated using
+ * a {@link SlidingPercentile} and is updated each time a transfer ends.
+ */
+public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener<Object> {
+
+ /**
+ * The default maximum weight for the sliding window.
+ */
+ public static final int DEFAULT_MAX_WEIGHT = 2000;
+
+ private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000;
+ private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024;
+
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final SlidingPercentile slidingPercentile;
+
+ private int streamCount;
+ private long sampleStartTimeMs;
+ private long sampleBytesTransferred;
+
+ private long totalElapsedTimeMs;
+ private long totalBytesTransferred;
+ private long bitrateEstimate;
+
+ public DefaultBandwidthMeter() {
+ this(null, null);
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) {
+ this(eventHandler, eventListener, DEFAULT_MAX_WEIGHT);
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) {
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.slidingPercentile = new SlidingPercentile(maxWeight);
+ bitrateEstimate = NO_ESTIMATE;
+ }
+
+ @Override
+ public synchronized long getBitrateEstimate() {
+ return bitrateEstimate;
+ }
+
+ @Override
+ public synchronized void onTransferStart(Object source, DataSpec dataSpec) {
+ if (streamCount == 0) {
+ sampleStartTimeMs = SystemClock.elapsedRealtime();
+ }
+ streamCount++;
+ }
+
+ @Override
+ public synchronized void onBytesTransferred(Object source, int bytes) {
+ sampleBytesTransferred += bytes;
+ }
+
+ @Override
+ public synchronized void onTransferEnd(Object source) {
+ Assertions.checkState(streamCount > 0);
+ long nowMs = SystemClock.elapsedRealtime();
+ int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
+ totalElapsedTimeMs += sampleElapsedTimeMs;
+ totalBytesTransferred += sampleBytesTransferred;
+ if (sampleElapsedTimeMs > 0) {
+ float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs;
+ slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
+ if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
+ || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
+ float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f);
+ bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE
+ : (long) bitrateEstimateFloat;
+ }
+ }
+ notifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);
+ if (--streamCount > 0) {
+ sampleStartTimeMs = nowMs;
+ }
+ sampleBytesTransferred = 0;
+ }
+
+ private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onBandwidthSample(elapsedMs, bytes, bitrate);
+ }
+ });
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java
@@ -0,0 +1,147 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.net.Uri;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link DataSource} that supports multiple URI schemes. The supported schemes are:
+ *
+ * <ul>
+ * <li>file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just
+ * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a
+ * local file URI).
+ * <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
+ * <li>content: For fetching data from a content URI (e.g. content://authority/path/123).
+ * <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if
+ * constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or
+ * any other schemes supported by a base data source if constructed using
+ * {@link #DefaultDataSource(Context, TransferListener, DataSource)}.
+ * </ul>
+ */
+public final class DefaultDataSource implements DataSource {
+
+ private static final String SCHEME_ASSET = "asset";
+ private static final String SCHEME_CONTENT = "content";
+
+ private final DataSource baseDataSource;
+ private final DataSource fileDataSource;
+ private final DataSource assetDataSource;
+ private final DataSource contentDataSource;
+
+ private DataSource dataSource;
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param listener An optional listener.
+ * @param userAgent The User-Agent string that should be used when requesting remote data.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled when fetching remote data.
+ */
+ public DefaultDataSource(Context context, TransferListener<? super DataSource> listener,
+ String userAgent, boolean allowCrossProtocolRedirects) {
+ this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects);
+ }
+
+ /**
+ * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+ *
+ * @param context A context.
+ * @param listener An optional listener.
+ * @param userAgent The User-Agent string that should be used when requesting remote data.
+ * @param connectTimeoutMillis The connection timeout that should be used when requesting remote
+ * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout that should be used when requesting remote data,
+ * in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled when fetching remote data.
+ */
+ public DefaultDataSource(Context context, TransferListener<? super DataSource> listener,
+ String userAgent, int connectTimeoutMillis, int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects) {
+ this(context, listener,
+ new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis,
+ readTimeoutMillis, allowCrossProtocolRedirects, null));
+ }
+
+ /**
+ * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other
+ * than file, asset and content.
+ *
+ * @param context A context.
+ * @param listener An optional listener.
+ * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and
+ * content. This {@link DataSource} should normally support at least http(s).
+ */
+ public DefaultDataSource(Context context, TransferListener<? super DataSource> listener,
+ DataSource baseDataSource) {
+ this.baseDataSource = Assertions.checkNotNull(baseDataSource);
+ this.fileDataSource = new FileDataSource(listener);
+ this.assetDataSource = new AssetDataSource(context, listener);
+ this.contentDataSource = new ContentDataSource(context, listener);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ Assertions.checkState(dataSource == null);
+ // Choose the correct source for the scheme.
+ String scheme = dataSpec.uri.getScheme();
+ if (Util.isLocalFileUri(dataSpec.uri)) {
+ if (dataSpec.uri.getPath().startsWith("/android_asset/")) {
+ dataSource = assetDataSource;
+ } else {
+ dataSource = fileDataSource;
+ }
+ } else if (SCHEME_ASSET.equals(scheme)) {
+ dataSource = assetDataSource;
+ } else if (SCHEME_CONTENT.equals(scheme)) {
+ dataSource = contentDataSource;
+ } else {
+ dataSource = baseDataSource;
+ }
+ // Open the source and return.
+ return dataSource.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ return dataSource.read(buffer, offset, readLength);
+ }
+
+ @Override
+ public Uri getUri() {
+ return dataSource == null ? null : dataSource.getUri();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (dataSource != null) {
+ try {
+ dataSource.close();
+ } finally {
+ dataSource = null;
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import com.google.android.exoplayer2.upstream.DataSource.Factory;
+
+/**
+ * A {@link Factory} that produces {@link DefaultDataSource} instances that delegate to
+ * {@link DefaultHttpDataSource}s for non-file/asset/content URIs.
+ */
+public final class DefaultDataSourceFactory implements Factory {
+
+ private final Context context;
+ private final TransferListener<? super DataSource> listener;
+ private final DataSource.Factory baseDataSourceFactory;
+
+ /**
+ * @param context A context.
+ * @param userAgent The User-Agent string that should be used.
+ */
+ public DefaultDataSourceFactory(Context context, String userAgent) {
+ this(context, userAgent, null);
+ }
+
+ /**
+ * @param context A context.
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ */
+ public DefaultDataSourceFactory(Context context, String userAgent,
+ TransferListener<? super DataSource> listener) {
+ this(context, listener, new DefaultHttpDataSourceFactory(userAgent, listener));
+ }
+
+ /**
+ * @param context A context.
+ * @param listener An optional listener.
+ * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource}
+ * for {@link DefaultDataSource}.
+ * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource)
+ */
+ public DefaultDataSourceFactory(Context context, TransferListener<? super DataSource> listener,
+ DataSource.Factory baseDataSourceFactory) {
+ this.context = context.getApplicationContext();
+ this.listener = listener;
+ this.baseDataSourceFactory = baseDataSourceFactory;
+ }
+
+ @Override
+ public DefaultDataSource createDataSource() {
+ return new DefaultDataSource(context, listener, baseDataSourceFactory.createDataSource());
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -0,0 +1,646 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Predicate;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.NoRouteToHostException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
+ * <p>
+ * By default this implementation will not follow cross-protocol redirects (i.e. redirects from
+ * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the
+ * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean,
+ * RequestProperties)} constructor and passing {@code true} as the second last argument.
+ */
+public class DefaultHttpDataSource implements HttpDataSource {
+
+ /**
+ * The default connection timeout, in milliseconds.
+ */
+ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
+ /**
+ * The default read timeout, in milliseconds.
+ */
+ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
+
+ private static final String TAG = "DefaultHttpDataSource";
+ private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
+ private static final long MAX_BYTES_TO_DRAIN = 2048;
+ private static final Pattern CONTENT_RANGE_HEADER =
+ Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
+ private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
+
+ private final boolean allowCrossProtocolRedirects;
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final String userAgent;
+ private final Predicate<String> contentTypePredicate;
+ private final RequestProperties defaultRequestProperties;
+ private final RequestProperties requestProperties;
+ private final TransferListener<? super DefaultHttpDataSource> listener;
+
+ private DataSpec dataSpec;
+ private HttpURLConnection connection;
+ private InputStream inputStream;
+ private boolean opened;
+
+ private long bytesToSkip;
+ private long bytesToRead;
+
+ private long bytesSkipped;
+ private long bytesRead;
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ */
+ public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate) {
+ this(userAgent, contentTypePredicate, null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ */
+ public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
+ TransferListener<? super DefaultHttpDataSource> listener) {
+ this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout.
+ */
+ public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
+ TransferListener<? super DefaultHttpDataSource> listener, int connectTimeoutMillis,
+ int readTimeoutMillis) {
+ this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false,
+ null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+ * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
+ * the default value.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ * @param defaultRequestProperties The default request properties to be sent to the server as
+ * HTTP headers or {@code null} if not required.
+ */
+ public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
+ TransferListener<? super DefaultHttpDataSource> listener, int connectTimeoutMillis,
+ int readTimeoutMillis, boolean allowCrossProtocolRedirects,
+ RequestProperties defaultRequestProperties) {
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.contentTypePredicate = contentTypePredicate;
+ this.listener = listener;
+ this.requestProperties = new RequestProperties();
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ this.defaultRequestProperties = defaultRequestProperties;
+ }
+
+ @Override
+ public Uri getUri() {
+ return connection == null ? null : Uri.parse(connection.getURL().toString());
+ }
+
+ @Override
+ public Map<String, List<String>> getResponseHeaders() {
+ return connection == null ? null : connection.getHeaderFields();
+ }
+
+ @Override
+ public void setRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ requestProperties.set(name, value);
+ }
+
+ @Override
+ public void clearRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ requestProperties.remove(name);
+ }
+
+ @Override
+ public void clearAllRequestProperties() {
+ requestProperties.clear();
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws HttpDataSourceException {
+ this.dataSpec = dataSpec;
+ this.bytesRead = 0;
+ this.bytesSkipped = 0;
+ try {
+ connection = makeConnection(dataSpec);
+ } catch (IOException e) {
+ throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
+ dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ int responseCode;
+ try {
+ responseCode = connection.getResponseCode();
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
+ dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ // Check for a valid response code.
+ if (responseCode < 200 || responseCode > 299) {
+ Map<String, List<String>> headers = connection.getHeaderFields();
+ closeConnectionQuietly();
+ InvalidResponseCodeException exception =
+ new InvalidResponseCodeException(responseCode, headers, dataSpec);
+ if (responseCode == 416) {
+ exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
+ }
+ throw exception;
+ }
+
+ // Check for a valid content type.
+ String contentType = connection.getContentType();
+ if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
+ closeConnectionQuietly();
+ throw new InvalidContentTypeException(contentType, dataSpec);
+ }
+
+ // If we requested a range starting from a non-zero position and received a 200 rather than a
+ // 206, then the server does not support partial requests. We'll need to manually skip to the
+ // requested position.
+ bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+
+ // Determine the length of the data to be read, after skipping.
+ if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesToRead = dataSpec.length;
+ } else {
+ long contentLength = getContentLength(connection);
+ bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
+ : C.LENGTH_UNSET;
+ }
+ } else {
+ // Gzip is enabled. If the server opts to use gzip then the content length in the response
+ // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a
+ // reliable way to determine whether the gzip was used or not. Always use the dataSpec length
+ // in this case.
+ bytesToRead = dataSpec.length;
+ }
+
+ try {
+ inputStream = connection.getInputStream();
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+
+ return bytesToRead;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
+ try {
+ skipInternal();
+ return readInternal(buffer, offset, readLength);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ @Override
+ public void close() throws HttpDataSourceException {
+ try {
+ if (inputStream != null) {
+ maybeTerminateInputStream(connection, bytesRemaining());
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE);
+ }
+ }
+ } finally {
+ inputStream = null;
+ closeConnectionQuietly();
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the current connection, or null if the source is not currently opened.
+ *
+ * @return The current open connection, or null.
+ */
+ protected final HttpURLConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the number of bytes that have been skipped since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes skipped.
+ */
+ protected final long bytesSkipped() {
+ return bytesSkipped;
+ }
+
+ /**
+ * Returns the number of bytes that have been read since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes read.
+ */
+ protected final long bytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
+ * <p>
+ * If the total length of the data being read is known, then this length minus {@code bytesRead()}
+ * is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.
+ *
+ * @return The remaining length, or {@link C#LENGTH_UNSET}.
+ */
+ protected final long bytesRemaining() {
+ return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
+ }
+
+ /**
+ * Establishes a connection, following redirects to do so where permitted.
+ */
+ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
+ URL url = new URL(dataSpec.uri.toString());
+ byte[] postBody = dataSpec.postBody;
+ long position = dataSpec.position;
+ long length = dataSpec.length;
+ boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
+
+ if (!allowCrossProtocolRedirects) {
+ // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
+ // automatically. This is the behavior we want, so use it.
+ return makeConnection(url, postBody, position, length, allowGzip, true /* followRedirects */);
+ }
+
+ // We need to handle redirects ourselves to allow cross-protocol redirects.
+ int redirectCount = 0;
+ while (redirectCount++ <= MAX_REDIRECTS) {
+ HttpURLConnection connection = makeConnection(
+ url, postBody, position, length, allowGzip, false /* followRedirects */);
+ int responseCode = connection.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
+ || responseCode == HttpURLConnection.HTTP_MOVED_PERM
+ || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
+ || responseCode == HttpURLConnection.HTTP_SEE_OTHER
+ || (postBody == null
+ && (responseCode == 307 /* HTTP_TEMP_REDIRECT */
+ || responseCode == 308 /* HTTP_PERM_REDIRECT */))) {
+ // For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into
+ // GET requests. For 307 and 308 POST requests are not redirected.
+ postBody = null;
+ String location = connection.getHeaderField("Location");
+ connection.disconnect();
+ url = handleRedirect(url, location);
+ } else {
+ return connection;
+ }
+ }
+
+ // If we get here we've been redirected more times than are permitted.
+ throw new NoRouteToHostException("Too many redirects: " + redirectCount);
+ }
+
+ /**
+ * Configures a connection and opens it.
+ *
+ * @param url The url to connect to.
+ * @param postBody The body data for a POST request.
+ * @param position The byte offset of the requested data.
+ * @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
+ * @param allowGzip Whether to allow the use of gzip.
+ * @param followRedirects Whether to follow redirects.
+ */
+ private HttpURLConnection makeConnection(URL url, byte[] postBody, long position,
+ long length, boolean allowGzip, boolean followRedirects) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setConnectTimeout(connectTimeoutMillis);
+ connection.setReadTimeout(readTimeoutMillis);
+ if (defaultRequestProperties != null) {
+ for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {
+ connection.setRequestProperty(property.getKey(), property.getValue());
+ }
+ }
+ for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {
+ connection.setRequestProperty(property.getKey(), property.getValue());
+ }
+ if (!(position == 0 && length == C.LENGTH_UNSET)) {
+ String rangeRequest = "bytes=" + position + "-";
+ if (length != C.LENGTH_UNSET) {
+ rangeRequest += (position + length - 1);
+ }
+ connection.setRequestProperty("Range", rangeRequest);
+ }
+ connection.setRequestProperty("User-Agent", userAgent);
+ if (!allowGzip) {
+ connection.setRequestProperty("Accept-Encoding", "identity");
+ }
+ connection.setInstanceFollowRedirects(followRedirects);
+ connection.setDoOutput(postBody != null);
+ if (postBody != null) {
+ connection.setRequestMethod("POST");
+ if (postBody.length == 0) {
+ connection.connect();
+ } else {
+ connection.setFixedLengthStreamingMode(postBody.length);
+ connection.connect();
+ OutputStream os = connection.getOutputStream();
+ os.write(postBody);
+ os.close();
+ }
+ } else {
+ connection.connect();
+ }
+ return connection;
+ }
+
+ /**
+ * Handles a redirect.
+ *
+ * @param originalUrl The original URL.
+ * @param location The Location header in the response.
+ * @return The next URL.
+ * @throws IOException If redirection isn't possible.
+ */
+ private static URL handleRedirect(URL originalUrl, String location) throws IOException {
+ if (location == null) {
+ throw new ProtocolException("Null location redirect");
+ }
+ // Form the new url.
+ URL url = new URL(originalUrl, location);
+ // Check that the protocol of the new url is supported.
+ String protocol = url.getProtocol();
+ if (!"https".equals(protocol) && !"http".equals(protocol)) {
+ throw new ProtocolException("Unsupported protocol redirect: " + protocol);
+ }
+ // Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
+ // below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
+ // redirects are disabled, we'll need to uncomment this block of code.
+ // if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
+ // throw new ProtocolException("Disallowed cross-protocol redirect ("
+ // + originalUrl.getProtocol() + " to " + protocol + ")");
+ // }
+ return url;
+ }
+
+ /**
+ * Attempts to extract the length of the content from the response headers of an open connection.
+ *
+ * @param connection The open connection.
+ * @return The extracted length, or {@link C#LENGTH_UNSET}.
+ */
+ private static long getContentLength(HttpURLConnection connection) {
+ long contentLength = C.LENGTH_UNSET;
+ String contentLengthHeader = connection.getHeaderField("Content-Length");
+ if (!TextUtils.isEmpty(contentLengthHeader)) {
+ try {
+ contentLength = Long.parseLong(contentLengthHeader);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
+ }
+ }
+ String contentRangeHeader = connection.getHeaderField("Content-Range");
+ if (!TextUtils.isEmpty(contentRangeHeader)) {
+ Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
+ if (matcher.find()) {
+ try {
+ long contentLengthFromRange =
+ Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
+ if (contentLength < 0) {
+ // Some proxy servers strip the Content-Length header. Fall back to the length
+ // calculated here in this case.
+ contentLength = contentLengthFromRange;
+ } else if (contentLength != contentLengthFromRange) {
+ // If there is a discrepancy between the Content-Length and Content-Range headers,
+ // assume the one with the larger value is correct. We have seen cases where carrier
+ // change one of them to reduce the size of a request, but it is unlikely anybody would
+ // increase it.
+ Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ + "]");
+ contentLength = Math.max(contentLength, contentLengthFromRange);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
+ }
+ }
+ }
+ return contentLength;
+ }
+
+ /**
+ * Skips any bytes that need skipping. Else does nothing.
+ * <p>
+ * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
+ *
+ * @throws InterruptedIOException If the thread is interrupted during the operation.
+ * @throws EOFException If the end of the input stream is reached before the bytes are skipped.
+ */
+ private void skipInternal() throws IOException {
+ if (bytesSkipped == bytesToSkip) {
+ return;
+ }
+
+ // Acquire the shared skip buffer.
+ byte[] skipBuffer = skipBufferReference.getAndSet(null);
+ if (skipBuffer == null) {
+ skipBuffer = new byte[4096];
+ }
+
+ while (bytesSkipped != bytesToSkip) {
+ int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
+ int read = inputStream.read(skipBuffer, 0, readLength);
+ if (Thread.interrupted()) {
+ throw new InterruptedIOException();
+ }
+ if (read == -1) {
+ throw new EOFException();
+ }
+ bytesSkipped += read;
+ if (listener != null) {
+ listener.onBytesTransferred(this, read);
+ }
+ }
+
+ // Release the shared skip buffer.
+ skipBufferReference.set(skipBuffer);
+ }
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}.
+ * <p>
+ * This method blocks until at least one byte of data can be read, the end of the opened range is
+ * detected, or an exception is thrown.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
+ * range is reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesToRead != C.LENGTH_UNSET) {
+ long bytesRemaining = bytesToRead - bytesRead;
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ readLength = (int) Math.min(readLength, bytesRemaining);
+ }
+
+ int read = inputStream.read(buffer, offset, readLength);
+ if (read == -1) {
+ if (bytesToRead != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new EOFException();
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ bytesRead += read;
+ if (listener != null) {
+ listener.onBytesTransferred(this, read);
+ }
+ return read;
+ }
+
+ /**
+ * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
+ * block for a long time if the stream has a lot of data remaining. Call this method before
+ * closing the input stream to make a best effort to cause the input stream to encounter an
+ * unexpected end of input, working around this issue. On other platform API levels, the method
+ * does nothing.
+ *
+ * @param connection The connection whose {@link InputStream} should be terminated.
+ * @param bytesRemaining The number of bytes remaining to be read from the input stream if its
+ * length is known. {@link C#LENGTH_UNSET} otherwise.
+ */
+ private static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
+ if (Util.SDK_INT != 19 && Util.SDK_INT != 20) {
+ return;
+ }
+
+ try {
+ InputStream inputStream = connection.getInputStream();
+ if (bytesRemaining == C.LENGTH_UNSET) {
+ // If the input stream has already ended, do nothing. The socket may be re-used.
+ if (inputStream.read() == -1) {
+ return;
+ }
+ } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
+ // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be
+ // re-used.
+ return;
+ }
+ String className = inputStream.getClass().getName();
+ if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream")
+ || className.equals(
+ "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) {
+ Class<?> superclass = inputStream.getClass().getSuperclass();
+ Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
+ unexpectedEndOfInput.setAccessible(true);
+ unexpectedEndOfInput.invoke(inputStream);
+ }
+ } catch (Exception e) {
+ // If an IOException then the connection didn't ever have an input stream, or it was closed
+ // already. If another type of exception then something went wrong, most likely the device
+ // isn't using okhttp.
+ }
+ }
+
+
+ /**
+ * Closes the current connection quietly, if there is one.
+ */
+ private void closeConnectionQuietly() {
+ if (connection != null) {
+ try {
+ connection.disconnect();
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error while disconnecting", e);
+ }
+ connection = null;
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+
+/** A {@link Factory} that produces {@link DefaultHttpDataSource} instances. */
+public final class DefaultHttpDataSourceFactory extends BaseFactory {
+
+ private final String userAgent;
+ private final TransferListener<? super DataSource> listener;
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final boolean allowCrossProtocolRedirects;
+
+ /**
+ * Constructs a DefaultHttpDataSourceFactory. Sets {@link
+ * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param userAgent The User-Agent string that should be used.
+ */
+ public DefaultHttpDataSourceFactory(String userAgent) {
+ this(userAgent, null);
+ }
+
+ /**
+ * Constructs a DefaultHttpDataSourceFactory. Sets {@link
+ * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
+ * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
+ * cross-protocol redirects.
+ *
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ * @see #DefaultHttpDataSourceFactory(String, TransferListener, int, int, boolean)
+ */
+ public DefaultHttpDataSourceFactory(
+ String userAgent, TransferListener<? super DataSource> listener) {
+ this(userAgent, listener, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, false);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout that should be used when requesting remote
+ * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in
+ * milliseconds. A timeout of zero is interpreted as an infinite timeout.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ */
+ public DefaultHttpDataSourceFactory(String userAgent,
+ TransferListener<? super DataSource> listener, int connectTimeoutMillis,
+ int readTimeoutMillis, boolean allowCrossProtocolRedirects) {
+ this.userAgent = userAgent;
+ this.listener = listener;
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ }
+
+ @Override
+ protected DefaultHttpDataSource createDataSourceInternal(
+ HttpDataSource.RequestProperties defaultRequestProperties) {
+ return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis,
+ readTimeoutMillis, allowCrossProtocolRedirects, defaultRequestProperties);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/DummyDataSource.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import java.io.IOException;
+
+/**
+ * A dummy DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}.
+ */
+public final class DummyDataSource implements DataSource {
+
+ public static final DummyDataSource INSTANCE = new DummyDataSource();
+
+ /** A factory that that produces {@link DummyDataSource}. */
+ public static final Factory FACTORY = new Factory() {
+ @Override
+ public DataSource createDataSource() {
+ return new DummyDataSource();
+ }
+ };
+
+ private DummyDataSource() {}
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ throw new IOException("Dummy source");
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri getUri() {
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // do nothing.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/FileDataSource.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * A {@link DataSource} for reading local files.
+ */
+public final class FileDataSource implements DataSource {
+
+ /**
+ * Thrown when IOException is encountered during local file read operation.
+ */
+ public static class FileDataSourceException extends IOException {
+
+ public FileDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final TransferListener<? super FileDataSource> listener;
+
+ private RandomAccessFile file;
+ private Uri uri;
+ private long bytesRemaining;
+ private boolean opened;
+
+ public FileDataSource() {
+ this(null);
+ }
+
+ /**
+ * @param listener An optional listener.
+ */
+ public FileDataSource(TransferListener<? super FileDataSource> listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws FileDataSourceException {
+ try {
+ uri = dataSpec.uri;
+ file = new RandomAccessFile(dataSpec.uri.getPath(), "r");
+ file.seek(dataSpec.position);
+ bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position
+ : dataSpec.length;
+ if (bytesRemaining < 0) {
+ throw new EOFException();
+ }
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ int bytesRead;
+ try {
+ bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength));
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+
+ if (bytesRead > 0) {
+ bytesRemaining -= bytesRead;
+ if (listener != null) {
+ listener.onBytesTransferred(this, bytesRead);
+ }
+ }
+
+ return bytesRead;
+ }
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws FileDataSourceException {
+ uri = null;
+ try {
+ if (file != null) {
+ file.close();
+ }
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ } finally {
+ file = null;
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link FileDataSource}.
+ */
+public final class FileDataSourceFactory implements DataSource.Factory {
+
+ private final TransferListener<? super FileDataSource> listener;
+
+ public FileDataSourceFactory() {
+ this(null);
+ }
+
+ public FileDataSourceFactory(TransferListener<? super FileDataSource> listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public DataSource createDataSource() {
+ return new FileDataSource(listener);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -0,0 +1,350 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.util.Predicate;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An HTTP {@link DataSource}.
+ */
+public interface HttpDataSource extends DataSource {
+
+ /**
+ * A factory for {@link HttpDataSource} instances.
+ */
+ interface Factory extends DataSource.Factory {
+
+ @Override
+ HttpDataSource createDataSource();
+
+ /**
+ * Gets the default request properties used by all {@link HttpDataSource}s created by the
+ * factory. Changes to the properties will be reflected in any future requests made by
+ * {@link HttpDataSource}s created by the factory.
+ *
+ * @return The default request properties of the factory.
+ */
+ RequestProperties getDefaultRequestProperties();
+
+ /**
+ * Sets a default request header for {@link HttpDataSource} instances created by the factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ @Deprecated
+ void setDefaultRequestProperty(String name, String value);
+
+ /**
+ * Clears a default request header for {@link HttpDataSource} instances created by the factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ * @param name The name of the header field.
+ */
+ @Deprecated
+ void clearDefaultRequestProperty(String name);
+
+ /**
+ * Clears all default request headers for all {@link HttpDataSource} instances created by the
+ * factory.
+ *
+ * @deprecated Use {@link #getDefaultRequestProperties} instead.
+ */
+ @Deprecated
+ void clearAllDefaultRequestProperties();
+
+ }
+
+ /**
+ * Stores HTTP request properties (aka HTTP headers) and provides methods to modify the headers
+ * in a thread safe way to avoid the potential of creating snapshots of an inconsistent or
+ * unintended state.
+ */
+ final class RequestProperties {
+
+ private final Map<String, String> requestProperties;
+ private Map<String, String> requestPropertiesSnapshot;
+
+ public RequestProperties() {
+ requestProperties = new HashMap<>();
+ }
+
+ /**
+ * Sets the specified property {@code value} for the specified {@code name}. If a property for
+ * this name previously existed, the old value is replaced by the specified value.
+ *
+ * @param name The name of the request property.
+ * @param value The value of the request property.
+ */
+ public synchronized void set(String name, String value) {
+ requestPropertiesSnapshot = null;
+ requestProperties.put(name, value);
+ }
+
+ /**
+ * Sets the keys and values contained in the map. If a property previously existed, the old
+ * value is replaced by the specified value. If a property previously existed and is not in the
+ * map, the property is left unchanged.
+ *
+ * @param properties The request properties.
+ */
+ public synchronized void set(Map<String, String> properties) {
+ requestPropertiesSnapshot = null;
+ requestProperties.putAll(properties);
+ }
+
+ /**
+ * Removes all properties previously existing and sets the keys and values of the map.
+ *
+ * @param properties The request properties.
+ */
+ public synchronized void clearAndSet(Map<String, String> properties) {
+ requestPropertiesSnapshot = null;
+ requestProperties.clear();
+ requestProperties.putAll(properties);
+ }
+
+ /**
+ * Removes a request property by name.
+ *
+ * @param name The name of the request property to remove.
+ */
+ public synchronized void remove(String name) {
+ requestPropertiesSnapshot = null;
+ requestProperties.remove(name);
+ }
+
+ /**
+ * Clears all request properties.
+ */
+ public synchronized void clear() {
+ requestPropertiesSnapshot = null;
+ requestProperties.clear();
+ }
+
+ /**
+ * Gets a snapshot of the request properties.
+ *
+ * @return A snapshot of the request properties.
+ */
+ public synchronized Map<String, String> getSnapshot() {
+ if (requestPropertiesSnapshot == null) {
+ requestPropertiesSnapshot = Collections.unmodifiableMap(new HashMap<>(requestProperties));
+ }
+ return requestPropertiesSnapshot;
+ }
+
+ }
+
+ /**
+ * Base implementation of {@link Factory} that sets default request properties.
+ */
+ abstract class BaseFactory implements Factory {
+
+ private final RequestProperties defaultRequestProperties;
+
+ public BaseFactory() {
+ defaultRequestProperties = new RequestProperties();
+ }
+
+ @Override
+ public final HttpDataSource createDataSource() {
+ return createDataSourceInternal(defaultRequestProperties);
+ }
+
+ @Override
+ public final RequestProperties getDefaultRequestProperties() {
+ return defaultRequestProperties;
+ }
+
+ @Deprecated
+ @Override
+ public final void setDefaultRequestProperty(String name, String value) {
+ defaultRequestProperties.set(name, value);
+ }
+
+ @Deprecated
+ @Override
+ public final void clearDefaultRequestProperty(String name) {
+ defaultRequestProperties.remove(name);
+ }
+
+ @Deprecated
+ @Override
+ public final void clearAllDefaultRequestProperties() {
+ defaultRequestProperties.clear();
+ }
+
+ /**
+ * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance.
+ *
+ * @param defaultRequestProperties The default {@code RequestProperties} to be used by the
+ * {@link HttpDataSource} instance.
+ * @return A {@link HttpDataSource} instance.
+ */
+ protected abstract HttpDataSource createDataSourceInternal(RequestProperties
+ defaultRequestProperties);
+
+ }
+
+ /**
+ * A {@link Predicate} that rejects content types often used for pay-walls.
+ */
+ Predicate<String> REJECT_PAYWALL_TYPES = new Predicate<String>() {
+
+ @Override
+ public boolean evaluate(String contentType) {
+ contentType = Util.toLowerInvariant(contentType);
+ return !TextUtils.isEmpty(contentType)
+ && (!contentType.contains("text") || contentType.contains("text/vtt"))
+ && !contentType.contains("html") && !contentType.contains("xml");
+ }
+
+ };
+
+ /**
+ * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}.
+ */
+ class HttpDataSourceException extends IOException {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_OPEN, TYPE_READ, TYPE_CLOSE})
+ public @interface Type {}
+ public static final int TYPE_OPEN = 1;
+ public static final int TYPE_READ = 2;
+ public static final int TYPE_CLOSE = 3;
+
+ @Type public final int type;
+
+ /**
+ * The {@link DataSpec} associated with the current connection.
+ */
+ public final DataSpec dataSpec;
+
+ public HttpDataSourceException(DataSpec dataSpec, @Type int type) {
+ super();
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) {
+ super(message);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) {
+ super(cause);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec,
+ @Type int type) {
+ super(message, cause);
+ this.dataSpec = dataSpec;
+ this.type = type;
+ }
+
+ }
+
+ /**
+ * Thrown when the content type is invalid.
+ */
+ final class InvalidContentTypeException extends HttpDataSourceException {
+
+ public final String contentType;
+
+ public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
+ super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN);
+ this.contentType = contentType;
+ }
+
+ }
+
+ /**
+ * Thrown when an attempt to open a connection results in a response code not in the 2xx range.
+ */
+ final class InvalidResponseCodeException extends HttpDataSourceException {
+
+ /**
+ * The response code that was outside of the 2xx range.
+ */
+ public final int responseCode;
+
+ /**
+ * An unmodifiable map of the response header fields and values.
+ */
+ public final Map<String, List<String>> headerFields;
+
+ public InvalidResponseCodeException(int responseCode, Map<String, List<String>> headerFields,
+ DataSpec dataSpec) {
+ super("Response code: " + responseCode, dataSpec, TYPE_OPEN);
+ this.responseCode = responseCode;
+ this.headerFields = headerFields;
+ }
+
+ }
+
+ @Override
+ long open(DataSpec dataSpec) throws HttpDataSourceException;
+
+ @Override
+ void close() throws HttpDataSourceException;
+
+ @Override
+ int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException;
+
+ /**
+ * Sets the value of a request header. The value will be used for subsequent connections
+ * established by the source.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ void setRequestProperty(String name, String value);
+
+ /**
+ * Clears the value of a request header. The change will apply to subsequent connections
+ * established by the source.
+ *
+ * @param name The name of the header field.
+ */
+ void clearRequestProperty(String name);
+
+ /**
+ * Clears all request headers that were set by {@link #setRequestProperty(String, String)}.
+ */
+ void clearAllRequestProperties();
+
+ /**
+ * Returns the headers provided in the response, or {@code null} if response headers are
+ * unavailable.
+ */
+ Map<String, List<String>> getResponseHeaders();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/Loader.java
@@ -0,0 +1,385 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages the background loading of {@link Loadable}s.
+ */
+public final class Loader implements LoaderErrorThrower {
+
+ /**
+ * Thrown when an unexpected exception is encountered during loading.
+ */
+ public static final class UnexpectedLoaderException extends IOException {
+
+ public UnexpectedLoaderException(Exception cause) {
+ super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
+ }
+
+ }
+
+ /**
+ * An object that can be loaded using a {@link Loader}.
+ */
+ public interface Loadable {
+
+ /**
+ * Cancels the load.
+ */
+ void cancelLoad();
+
+ /**
+ * Returns whether the load has been canceled.
+ */
+ boolean isLoadCanceled();
+
+ /**
+ * Performs the load, returning on completion or cancellation.
+ *
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ void load() throws IOException, InterruptedException;
+
+ }
+
+ /**
+ * A callback to be notified of {@link Loader} events.
+ */
+ public interface Callback<T extends Loadable> {
+
+ /**
+ * Called when a load has completed.
+ * <p>
+ * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and
+ * this callback being called.
+ *
+ * @param loadable The loadable whose load has completed.
+ * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load ended.
+ * @param loadDurationMs The duration of the load.
+ */
+ void onLoadCompleted(T loadable, long elapsedRealtimeMs, long loadDurationMs);
+
+ /**
+ * Called when a load has been canceled.
+ * <p>
+ * Note: If the {@link Loader} has not been released then there is guaranteed to be a memory
+ * barrier between {@link Loadable#load()} exiting and this callback being called. If the
+ * {@link Loader} has been released then this callback may be called before
+ * {@link Loadable#load()} exits.
+ *
+ * @param loadable The loadable whose load has been canceled.
+ * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the load was canceled.
+ * @param loadDurationMs The duration of the load up to the point at which it was canceled.
+ * @param released True if the load was canceled because the {@link Loader} was released. False
+ * otherwise.
+ */
+ void onLoadCanceled(T loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released);
+
+ /**
+ * Called when a load encounters an error.
+ * <p>
+ * Note: There is guaranteed to be a memory barrier between {@link Loadable#load()} exiting and
+ * this callback being called.
+ *
+ * @param loadable The loadable whose load has encountered an error.
+ * @param elapsedRealtimeMs {@link SystemClock#elapsedRealtime} when the error occurred.
+ * @param loadDurationMs The duration of the load up to the point at which the error occurred.
+ * @param error The load error.
+ * @return The desired retry action. One of {@link Loader#RETRY},
+ * {@link Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY} and
+ * {@link Loader#DONT_RETRY_FATAL}.
+ */
+ int onLoadError(T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error);
+
+ }
+
+ public static final int RETRY = 0;
+ public static final int RETRY_RESET_ERROR_COUNT = 1;
+ public static final int DONT_RETRY = 2;
+ public static final int DONT_RETRY_FATAL = 3;
+
+ private static final int MSG_START = 0;
+ private static final int MSG_CANCEL = 1;
+ private static final int MSG_END_OF_SOURCE = 2;
+ private static final int MSG_IO_EXCEPTION = 3;
+ private static final int MSG_FATAL_ERROR = 4;
+
+ private final ExecutorService downloadExecutorService;
+
+ private LoadTask<? extends Loadable> currentTask;
+ private IOException fatalError;
+
+ /**
+ * @param threadName A name for the loader's thread.
+ */
+ public Loader(String threadName) {
+ this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
+ }
+
+ /**
+ * Starts loading a {@link Loadable}.
+ * <p>
+ * The calling thread must be a {@link Looper} thread, which is the thread on which the
+ * {@link Callback} will be called.
+ *
+ * @param <T> The type of the loadable.
+ * @param loadable The {@link Loadable} to load.
+ * @param callback A callback to called when the load ends.
+ * @param defaultMinRetryCount The minimum number of times the load must be retried before
+ * {@link #maybeThrowError()} will propagate an error.
+ * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
+ * @return {@link SystemClock#elapsedRealtime} when the load started.
+ */
+ public <T extends Loadable> long startLoading(T loadable, Callback<T> callback,
+ int defaultMinRetryCount) {
+ Looper looper = Looper.myLooper();
+ Assertions.checkState(looper != null);
+ long startTimeMs = SystemClock.elapsedRealtime();
+ new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
+ return startTimeMs;
+ }
+
+ /**
+ * Returns whether the {@link Loader} is currently loading a {@link Loadable}.
+ */
+ public boolean isLoading() {
+ return currentTask != null;
+ }
+
+ /**
+ * Cancels the current load. This method should only be called when a load is in progress.
+ */
+ public void cancelLoading() {
+ currentTask.cancel(false);
+ }
+
+ /**
+ * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer
+ * required.
+ */
+ public void release() {
+ release(null);
+ }
+
+ /**
+ * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should
+ * be called when the {@link Loader} is no longer required.
+ *
+ * @param postLoadAction A {@link Runnable} to run on the loader's thread when
+ * {@link Loadable#load()} is no longer running.
+ */
+ public void release(Runnable postLoadAction) {
+ if (currentTask != null) {
+ currentTask.cancel(true);
+ }
+ if (postLoadAction != null) {
+ downloadExecutorService.execute(postLoadAction);
+ }
+ downloadExecutorService.shutdown();
+ }
+
+ // LoaderErrorThrower implementation.
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ maybeThrowError(Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ if (fatalError != null) {
+ throw fatalError;
+ } else if (currentTask != null) {
+ currentTask.maybeThrowError(minRetryCount == Integer.MIN_VALUE
+ ? currentTask.defaultMinRetryCount : minRetryCount);
+ }
+ }
+
+ // Internal classes.
+
+ @SuppressLint("HandlerLeak")
+ private final class LoadTask<T extends Loadable> extends Handler implements Runnable {
+
+ private static final String TAG = "LoadTask";
+
+ private final T loadable;
+ private final Loader.Callback<T> callback;
+ public final int defaultMinRetryCount;
+ private final long startTimeMs;
+
+ private IOException currentError;
+ private int errorCount;
+
+ private volatile Thread executorThread;
+ private volatile boolean released;
+
+ public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
+ int defaultMinRetryCount, long startTimeMs) {
+ super(looper);
+ this.loadable = loadable;
+ this.callback = callback;
+ this.defaultMinRetryCount = defaultMinRetryCount;
+ this.startTimeMs = startTimeMs;
+ }
+
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ if (currentError != null && errorCount > minRetryCount) {
+ throw currentError;
+ }
+ }
+
+ public void start(long delayMillis) {
+ Assertions.checkState(currentTask == null);
+ currentTask = this;
+ if (delayMillis > 0) {
+ sendEmptyMessageDelayed(MSG_START, delayMillis);
+ } else {
+ execute();
+ }
+ }
+
+ public void cancel(boolean released) {
+ this.released = released;
+ currentError = null;
+ if (hasMessages(MSG_START)) {
+ removeMessages(MSG_START);
+ if (!released) {
+ sendEmptyMessage(MSG_CANCEL);
+ }
+ } else {
+ loadable.cancelLoad();
+ if (executorThread != null) {
+ executorThread.interrupt();
+ }
+ }
+ if (released) {
+ finish();
+ long nowMs = SystemClock.elapsedRealtime();
+ callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ executorThread = Thread.currentThread();
+ if (!loadable.isLoadCanceled()) {
+ TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
+ try {
+ loadable.load();
+ } finally {
+ TraceUtil.endSection();
+ }
+ }
+ if (!released) {
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ }
+ } catch (IOException e) {
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget();
+ }
+ } catch (InterruptedException e) {
+ // The load was canceled.
+ Assertions.checkState(loadable.isLoadCanceled());
+ if (!released) {
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ }
+ } catch (Exception e) {
+ // This should never happen, but handle it anyway.
+ Log.e(TAG, "Unexpected exception loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget();
+ }
+ } catch (Error e) {
+ // We'd hope that the platform would kill the process if an Error is thrown here, but the
+ // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from
+ // the handler thread so that the process dies even if the executor behaves in this way.
+ Log.e(TAG, "Unexpected error loading stream", e);
+ if (!released) {
+ obtainMessage(MSG_FATAL_ERROR, e).sendToTarget();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (released) {
+ return;
+ }
+ if (msg.what == MSG_START) {
+ execute();
+ return;
+ }
+ if (msg.what == MSG_FATAL_ERROR) {
+ throw (Error) msg.obj;
+ }
+ finish();
+ long nowMs = SystemClock.elapsedRealtime();
+ long durationMs = nowMs - startTimeMs;
+ if (loadable.isLoadCanceled()) {
+ callback.onLoadCanceled(loadable, nowMs, durationMs, false);
+ return;
+ }
+ switch (msg.what) {
+ case MSG_CANCEL:
+ callback.onLoadCanceled(loadable, nowMs, durationMs, false);
+ break;
+ case MSG_END_OF_SOURCE:
+ callback.onLoadCompleted(loadable, nowMs, durationMs);
+ break;
+ case MSG_IO_EXCEPTION:
+ currentError = (IOException) msg.obj;
+ int retryAction = callback.onLoadError(loadable, nowMs, durationMs, currentError);
+ if (retryAction == DONT_RETRY_FATAL) {
+ fatalError = currentError;
+ } else if (retryAction != DONT_RETRY) {
+ errorCount = retryAction == RETRY_RESET_ERROR_COUNT ? 1 : errorCount + 1;
+ start(getRetryDelayMillis());
+ }
+ break;
+ }
+ }
+
+ private void execute() {
+ currentError = null;
+ downloadExecutorService.execute(currentTask);
+ }
+
+ private void finish() {
+ currentTask = null;
+ }
+
+ private long getRetryDelayMillis() {
+ return Math.min((errorCount - 1) * 1000, 5000);
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import java.io.IOException;
+
+/**
+ * Conditionally throws errors affecting a {@link Loader}.
+ */
+public interface LoaderErrorThrower {
+
+ /**
+ * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current
+ * {@link Loadable} has incurred a number of errors greater than the {@link Loader}s default
+ * minimum number of retries. Else does nothing.
+ *
+ * @throws IOException The error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
+ * Throws a fatal error, or a non-fatal error if loading is currently backed off and the current
+ * {@link Loadable} has incurred a number of errors greater than the specified minimum number
+ * of retries. Else does nothing.
+ *
+ * @param minRetryCount A minimum retry count that must be exceeded for a non-fatal error to be
+ * thrown. Should be non-negative.
+ * @throws IOException The error.
+ */
+ void maybeThrowError(int minRetryCount) throws IOException;
+
+ /**
+ * A {@link LoaderErrorThrower} that never throws.
+ */
+ final class Dummy implements LoaderErrorThrower {
+
+ @Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public void maybeThrowError(int minRetryCount) throws IOException {
+ // Do nothing.
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link Loadable} for objects that can be parsed from binary data using a {@link Parser}.
+ *
+ * @param <T> The type of the object being loaded.
+ */
+public final class ParsingLoadable<T> implements Loadable {
+
+ /**
+ * Parses an object from loaded data.
+ */
+ public interface Parser<T> {
+
+ /**
+ * Parses an object from a response.
+ *
+ * @param uri The source {@link Uri} of the response, after any redirection.
+ * @param inputStream An {@link InputStream} from which the response data can be read.
+ * @return The parsed object.
+ * @throws ParserException If an error occurs parsing the data.
+ * @throws IOException If an error occurs reading data from the stream.
+ */
+ T parse(Uri uri, InputStream inputStream) throws IOException;
+
+ }
+
+ /**
+ * The {@link DataSpec} that defines the data to be loaded.
+ */
+ public final DataSpec dataSpec;
+ /**
+ * The type of the data. One of the {@code DATA_TYPE_*} constants defined in {@link C}. For
+ * reporting only.
+ */
+ public final int type;
+
+ private final DataSource dataSource;
+ private final Parser<? extends T> parser;
+
+ private volatile T result;
+ private volatile boolean isCanceled;
+ private volatile long bytesLoaded;
+
+ /**
+ * @param dataSource A {@link DataSource} to use when loading the data.
+ * @param uri The {@link Uri} from which the object should be loaded.
+ * @param type See {@link #type}.
+ * @param parser Parses the object from the response.
+ */
+ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser<? extends T> parser) {
+ this.dataSource = dataSource;
+ this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP);
+ this.type = type;
+ this.parser = parser;
+ }
+
+ /**
+ * Returns the loaded object, or null if an object has not been loaded.
+ */
+ public final T getResult() {
+ return result;
+ }
+
+ /**
+ * Returns the number of bytes loaded. In the case that the network response was compressed, the
+ * value returned is the size of the data <em>after</em> decompression.
+ *
+ * @return The number of bytes loaded.
+ */
+ public long bytesLoaded() {
+ return bytesLoaded;
+ }
+
+ @Override
+ public final void cancelLoad() {
+ // We don't actually cancel anything, but we need to record the cancellation so that
+ // isLoadCanceled can return the correct value.
+ isCanceled = true;
+ }
+
+ @Override
+ public final boolean isLoadCanceled() {
+ return isCanceled;
+ }
+
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+ try {
+ inputStream.open();
+ result = parser.parse(dataSource.getUri(), inputStream);
+ } finally {
+ bytesLoaded = inputStream.bytesRead();
+ Util.closeQuietly(inputStream);
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+import java.io.IOException;
+
+/**
+ * A {@link DataSource} that can be used as part of a task registered with a
+ * {@link PriorityTaskManager}.
+ * <p>
+ * Calls to {@link #open(DataSpec)} and {@link #read(byte[], int, int)} are allowed to proceed only
+ * if there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there
+ * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} is thrown.
+ * <p>
+ * Instances of this class are intended to be used as parts of (possibly larger) tasks that are
+ * registered with the {@link PriorityTaskManager}, and hence do <em>not</em> register as tasks
+ * themselves.
+ */
+public final class PriorityDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final PriorityTaskManager priorityTaskManager;
+ private final int priority;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param priorityTaskManager The priority manager to which the task is registered.
+ * @param priority The priority of the task.
+ */
+ public PriorityDataSource(DataSource upstream, PriorityTaskManager priorityTaskManager,
+ int priority) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.priorityTaskManager = Assertions.checkNotNull(priorityTaskManager);
+ this.priority = priority;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ priorityTaskManager.proceedOrThrow(priority);
+ return upstream.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ priorityTaskManager.proceedOrThrow(priority);
+ return upstream.read(buffer, offset, max);
+ }
+
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/PriorityDataSourceFactory.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.upstream.DataSource.Factory;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link PriorityDataSource} instances.
+ */
+public final class PriorityDataSourceFactory implements Factory {
+
+ private final Factory upstreamFactory;
+ private final PriorityTaskManager priorityTaskManager;
+ private final int priority;
+
+ /**
+ * @param upstreamFactory A {@link DataSource.Factory} to be used to create an upstream {@link
+ * DataSource} for {@link PriorityDataSource}.
+ * @param priorityTaskManager The priority manager to which PriorityDataSource task is registered.
+ * @param priority The priority of PriorityDataSource task.
+ */
+ public PriorityDataSourceFactory(Factory upstreamFactory, PriorityTaskManager priorityTaskManager,
+ int priority) {
+ this.upstreamFactory = upstreamFactory;
+ this.priorityTaskManager = priorityTaskManager;
+ this.priority = priority;
+ }
+
+ @Override
+ public PriorityDataSource createDataSource() {
+ return new PriorityDataSource(upstreamFactory.createDataSource(), priorityTaskManager,
+ priority);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
@@ -0,0 +1,200 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading a raw resource inside the APK.
+ * <p>
+ * URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where
+ * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can
+ * be used to build {@link Uri}s in this format.
+ */
+public final class RawResourceDataSource implements DataSource {
+
+ /**
+ * Thrown when an {@link IOException} is encountered reading from a raw resource.
+ */
+ public static class RawResourceDataSourceException extends IOException {
+ public RawResourceDataSourceException(String message) {
+ super(message);
+ }
+
+ public RawResourceDataSourceException(IOException e) {
+ super(e);
+ }
+ }
+
+ /**
+ * Builds a {@link Uri} for the specified raw resource identifier.
+ *
+ * @param rawResourceId A raw resource identifier (i.e. a constant defined in {@code R.raw}).
+ * @return The corresponding {@link Uri}.
+ */
+ public static Uri buildRawResourceUri(int rawResourceId) {
+ return Uri.parse(RAW_RESOURCE_SCHEME + ":///" + rawResourceId);
+ }
+
+ private static final String RAW_RESOURCE_SCHEME = "rawresource";
+
+ private final Resources resources;
+ private final TransferListener<? super RawResourceDataSource> listener;
+
+ private Uri uri;
+ private AssetFileDescriptor assetFileDescriptor;
+ private InputStream inputStream;
+ private long bytesRemaining;
+ private boolean opened;
+
+ /**
+ * @param context A context.
+ */
+ public RawResourceDataSource(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * @param context A context.
+ * @param listener An optional listener.
+ */
+ public RawResourceDataSource(Context context,
+ TransferListener<? super RawResourceDataSource> listener) {
+ this.resources = context.getResources();
+ this.listener = listener;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
+ try {
+ uri = dataSpec.uri;
+ if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) {
+ throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME);
+ }
+
+ int resourceId;
+ try {
+ resourceId = Integer.parseInt(uri.getLastPathSegment());
+ } catch (NumberFormatException e) {
+ throw new RawResourceDataSourceException("Resource identifier must be an integer.");
+ }
+
+ assetFileDescriptor = resources.openRawResourceFd(resourceId);
+ inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
+ inputStream.skip(assetFileDescriptor.getStartOffset());
+ long skipped = inputStream.skip(dataSpec.position);
+ if (skipped < dataSpec.position) {
+ // We expect the skip to be satisfied in full. If it isn't then we're probably trying to
+ // skip beyond the end of the data.
+ throw new EOFException();
+ }
+ if (dataSpec.length != C.LENGTH_UNSET) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ long assetFileDescriptorLength = assetFileDescriptor.getLength();
+ // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
+ bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH
+ ? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position);
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+
+ return bytesRemaining;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws RawResourceDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ int bytesRead;
+ try {
+ int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength
+ : (int) Math.min(bytesRemaining, readLength);
+ bytesRead = inputStream.read(buffer, offset, bytesToRead);
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ }
+
+ if (bytesRead == -1) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ // End of stream reached having not read sufficient data.
+ throw new RawResourceDataSourceException(new EOFException());
+ }
+ return C.RESULT_END_OF_INPUT;
+ }
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ if (listener != null) {
+ listener.onBytesTransferred(this, bytesRead);
+ }
+ return bytesRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() throws RawResourceDataSourceException {
+ uri = null;
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ } finally {
+ inputStream = null;
+ try {
+ if (assetFileDescriptor != null) {
+ assetFileDescriptor.close();
+ }
+ } catch (IOException e) {
+ throw new RawResourceDataSourceException(e);
+ } finally {
+ assetFileDescriptor = null;
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/TeeDataSource.java
@@ -0,0 +1,76 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Tees data into a {@link DataSink} as the data is read.
+ */
+public final class TeeDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final DataSink dataSink;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param dataSink The {@link DataSink} into which data is written.
+ */
+ public TeeDataSource(DataSource upstream, DataSink dataSink) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.dataSink = Assertions.checkNotNull(dataSink);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ long dataLength = upstream.open(dataSpec);
+ if (dataSpec.length == C.LENGTH_UNSET && dataLength != C.LENGTH_UNSET) {
+ // Reconstruct dataSpec in order to provide the resolved length to the sink.
+ dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position,
+ dataLength, dataSpec.key, dataSpec.flags);
+ }
+ dataSink.open(dataSpec);
+ return dataLength;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ int num = upstream.read(buffer, offset, max);
+ if (num > 0) {
+ // TODO: Consider continuing even if disk writes fail.
+ dataSink.write(buffer, offset, num);
+ }
+ return num;
+ }
+
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ upstream.close();
+ } finally {
+ dataSink.close();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/TransferListener.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 com.google.android.exoplayer2.upstream;
+
+/**
+ * A listener of data transfer events.
+ */
+public interface TransferListener<S> {
+
+ /**
+ * Called when a transfer starts.
+ *
+ * @param source The source performing the transfer.
+ * @param dataSpec Describes the data being transferred.
+ */
+ void onTransferStart(S source, DataSpec dataSpec);
+
+ /**
+ * Called incrementally during a transfer.
+ *
+ * @param source The source performing the transfer.
+ * @param bytesTransferred The number of bytes transferred since the previous call to this
+ * method (or if the first call, since the transfer was started).
+ */
+ void onBytesTransferred(S source, int bytesTransferred);
+
+ /**
+ * Called when a transfer ends.
+ *
+ * @param source The source performing the transfer.
+ */
+ void onTransferEnd(S source);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/UdpDataSource.java
@@ -0,0 +1,187 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+
+/**
+ * A UDP {@link DataSource}.
+ */
+public final class UdpDataSource implements DataSource {
+
+ /**
+ * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}.
+ */
+ public static final class UdpDataSourceException extends IOException {
+
+ public UdpDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * The default maximum datagram packet size, in bytes.
+ */
+ public static final int DEFAULT_MAX_PACKET_SIZE = 2000;
+
+ /**
+ * The default socket timeout, in milliseconds.
+ */
+ public static final int DEAFULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000;
+
+ private final TransferListener<? super UdpDataSource> listener;
+ private final int socketTimeoutMillis;
+ private final byte[] packetBuffer;
+ private final DatagramPacket packet;
+
+ private Uri uri;
+ private DatagramSocket socket;
+ private MulticastSocket multicastSocket;
+ private InetAddress address;
+ private InetSocketAddress socketAddress;
+ private boolean opened;
+
+ private int packetRemaining;
+
+ /**
+ * @param listener An optional listener.
+ */
+ public UdpDataSource(TransferListener<? super UdpDataSource> listener) {
+ this(listener, DEFAULT_MAX_PACKET_SIZE);
+ }
+
+ /**
+ * @param listener An optional listener.
+ * @param maxPacketSize The maximum datagram packet size, in bytes.
+ */
+ public UdpDataSource(TransferListener<? super UdpDataSource> listener, int maxPacketSize) {
+ this(listener, maxPacketSize, DEAFULT_SOCKET_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * @param listener An optional listener.
+ * @param maxPacketSize The maximum datagram packet size, in bytes.
+ * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout.
+ */
+ public UdpDataSource(TransferListener<? super UdpDataSource> listener, int maxPacketSize,
+ int socketTimeoutMillis) {
+ this.listener = listener;
+ this.socketTimeoutMillis = socketTimeoutMillis;
+ packetBuffer = new byte[maxPacketSize];
+ packet = new DatagramPacket(packetBuffer, 0, maxPacketSize);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws UdpDataSourceException {
+ uri = dataSpec.uri;
+ String host = uri.getHost();
+ int port = uri.getPort();
+
+ try {
+ address = InetAddress.getByName(host);
+ socketAddress = new InetSocketAddress(address, port);
+ if (address.isMulticastAddress()) {
+ multicastSocket = new MulticastSocket(socketAddress);
+ multicastSocket.joinGroup(address);
+ socket = multicastSocket;
+ } else {
+ socket = new DatagramSocket(socketAddress);
+ }
+ } catch (IOException e) {
+ throw new UdpDataSourceException(e);
+ }
+
+ try {
+ socket.setSoTimeout(socketTimeoutMillis);
+ } catch (SocketException e) {
+ throw new UdpDataSourceException(e);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart(this, dataSpec);
+ }
+ return C.LENGTH_UNSET;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceException {
+ if (readLength == 0) {
+ return 0;
+ }
+
+ if (packetRemaining == 0) {
+ // We've read all of the data from the current packet. Get another.
+ try {
+ socket.receive(packet);
+ } catch (IOException e) {
+ throw new UdpDataSourceException(e);
+ }
+ packetRemaining = packet.getLength();
+ if (listener != null) {
+ listener.onBytesTransferred(this, packetRemaining);
+ }
+ }
+
+ int packetOffset = packet.getLength() - packetRemaining;
+ int bytesToRead = Math.min(packetRemaining, readLength);
+ System.arraycopy(packetBuffer, packetOffset, buffer, offset, bytesToRead);
+ packetRemaining -= bytesToRead;
+ return bytesToRead;
+ }
+
+ @Override
+ public Uri getUri() {
+ return uri;
+ }
+
+ @Override
+ public void close() {
+ uri = null;
+ if (multicastSocket != null) {
+ try {
+ multicastSocket.leaveGroup(address);
+ } catch (IOException e) {
+ // Do nothing.
+ }
+ multicastSocket = null;
+ }
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ address = null;
+ socketAddress = null;
+ packetRemaining = 0;
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd(this);
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/Cache.java
@@ -0,0 +1,229 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.NavigableSet;
+import java.util.Set;
+
+/**
+ * An interface for cache.
+ */
+public interface Cache {
+
+ /**
+ * Listener of {@link Cache} events.
+ */
+ interface Listener {
+
+ /**
+ * Called when a {@link CacheSpan} is added to the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The added {@link CacheSpan}.
+ */
+ void onSpanAdded(Cache cache, CacheSpan span);
+
+ /**
+ * Called when a {@link CacheSpan} is removed from the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The removed {@link CacheSpan}.
+ */
+ void onSpanRemoved(Cache cache, CacheSpan span);
+
+ /**
+ * Called when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new
+ * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however
+ * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed.
+ * <p>
+ * Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and
+ * {@link #onSpanRemoved(Cache, CacheSpan)} are not called in addition to this method.
+ *
+ * @param cache The source of the event.
+ * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache.
+ * @param newSpan The new {@link CacheSpan}, which has been added to the cache.
+ */
+ void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
+
+ }
+
+ /**
+ * Thrown when an error is encountered when writing data.
+ */
+ class CacheException extends IOException {
+
+ public CacheException(String message) {
+ super(message);
+ }
+
+ public CacheException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * Registers a listener to listen for changes to a given key.
+ * <p>
+ * No guarantees are made about the thread or threads on which the listener is called, but it is
+ * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in
+ * the same order as events occurred.
+ *
+ * @param key The key to listen to.
+ * @param listener The listener to add.
+ * @return The current spans for the key.
+ */
+ NavigableSet<CacheSpan> addListener(String key, Listener listener);
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param key The key to stop listening to.
+ * @param listener The listener to remove.
+ */
+ void removeListener(String key, Listener listener);
+
+ /**
+ * Returns the cached spans for a given cache key.
+ *
+ * @param key The key for which spans should be returned.
+ * @return The spans for the key. May be null if there are no such spans.
+ */
+ NavigableSet<CacheSpan> getCachedSpans(String key);
+
+ /**
+ * Returns all keys in the cache.
+ *
+ * @return All the keys in the cache.
+ */
+ Set<String> getKeys();
+
+ /**
+ * Returns the total disk space in bytes used by the cache.
+ *
+ * @return The total disk space in bytes.
+ */
+ long getCacheSpace();
+
+ /**
+ * A caller should invoke this method when they require data from a given position for a given
+ * key.
+ * <p>
+ * If there is a cache entry that overlaps the position, then the returned {@link CacheSpan}
+ * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller
+ * may read from the cache file, but does not acquire any locks.
+ * <p>
+ * If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan}
+ * defines a hole in the cache starting at {@code position} into which the caller may write as it
+ * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock.
+ * Whilst the caller holds the lock it may write data into the hole. It may split data into
+ * multiple files. When the caller has finished writing a file it should commit it to the cache
+ * by calling {@link #commitFile(File)}. When the caller has finished writing, it must release
+ * the lock by calling {@link #releaseHoleSpan}.
+ *
+ * @param key The key of the data being requested.
+ * @param position The position of the data being requested.
+ * @return The {@link CacheSpan}.
+ * @throws InterruptedException
+ */
+ CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException;
+
+ /**
+ * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then
+ * instead of blocking, this method will return null as the {@link CacheSpan}.
+ *
+ * @param key The key of the data being requested.
+ * @param position The position of the data being requested.
+ * @return The {@link CacheSpan}. Or null if the cache entry is locked.
+ */
+ CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException;
+
+ /**
+ * Obtains a cache file into which data can be written. Must only be called when holding a
+ * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param maxLength The maximum length of the data to be written. Used only to ensure that there
+ * is enough space in the cache.
+ * @return The file into which data should be written.
+ */
+ File startFile(String key, long position, long maxLength) throws CacheException;
+
+ /**
+ * Commits a file into the cache. Must only be called when holding a corresponding hole
+ * {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}
+ *
+ * @param file A newly written cache file.
+ */
+ void commitFile(File file) throws CacheException;
+
+ /**
+ * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
+ * corresponded to a hole in the cache.
+ *
+ * @param holeSpan The {@link CacheSpan} being released.
+ */
+ void releaseHoleSpan(CacheSpan holeSpan);
+
+ /**
+ * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file.
+ *
+ * @param span The {@link CacheSpan} to remove.
+ */
+ void removeSpan(CacheSpan span) throws CacheException;
+
+ /**
+ * Queries if a range is entirely available in the cache.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param length The length of the data.
+ * @return true if the data is available in the Cache otherwise false;
+ */
+ boolean isCached(String key, long position, long length);
+
+ /**
+ * Returns the length of the cached data block starting from the {@code position} to the block end
+ * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap
+ * to the next cached data up to {@code length} bytes) is returned.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param length The maximum length of the data to be returned.
+ * @return the length of the cached or not cached data block length.
+ */
+ long getCachedBytes(String key, long position, long length);
+
+ /**
+ * Sets the content length for the given key.
+ *
+ * @param key The cache key for the data.
+ * @param length The length of the data.
+ */
+ void setContentLength(String key, long length) throws CacheException;
+
+ /**
+ * Returns the content length for the given key if one set, or {@link
+ * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
+ *
+ * @param key The cache key for the data.
+ */
+ long getContentLength(String key);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -0,0 +1,183 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
+import com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Writes data into a cache.
+ */
+public final class CacheDataSink implements DataSink {
+
+ /** Default buffer size. */
+ public static final int DEFAULT_BUFFER_SIZE = 20480;
+
+ private final Cache cache;
+ private final long maxCacheFileSize;
+ private final int bufferSize;
+
+ private DataSpec dataSpec;
+ private File file;
+ private OutputStream outputStream;
+ private FileOutputStream underlyingFileOutputStream;
+ private long outputStreamBytesWritten;
+ private long dataSpecBytesWritten;
+ private ReusableBufferedOutputStream bufferedOutputStream;
+
+ /**
+ * Thrown when IOException is encountered when writing data into sink.
+ */
+ public static class CacheDataSinkException extends CacheException {
+
+ public CacheDataSinkException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}.
+ *
+ * @param cache The cache into which data should be written.
+ * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
+ * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
+ * multiple cache files.
+ */
+ public CacheDataSink(Cache cache, long maxCacheFileSize) {
+ this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * @param cache The cache into which data should be written.
+ * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
+ * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
+ * multiple cache files.
+ * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative
+ * value disables buffering.
+ */
+ public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) {
+ this.cache = Assertions.checkNotNull(cache);
+ this.maxCacheFileSize = maxCacheFileSize;
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public void open(DataSpec dataSpec) throws CacheDataSinkException {
+ if (dataSpec.length == C.LENGTH_UNSET
+ && !dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)) {
+ this.dataSpec = null;
+ return;
+ }
+ this.dataSpec = dataSpec;
+ dataSpecBytesWritten = 0;
+ try {
+ openNextOutputStream();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
+ if (dataSpec == null) {
+ return;
+ }
+ try {
+ int bytesWritten = 0;
+ while (bytesWritten < length) {
+ if (outputStreamBytesWritten == maxCacheFileSize) {
+ closeCurrentOutputStream();
+ openNextOutputStream();
+ }
+ int bytesToWrite = (int) Math.min(length - bytesWritten,
+ maxCacheFileSize - outputStreamBytesWritten);
+ outputStream.write(buffer, offset + bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ outputStreamBytesWritten += bytesToWrite;
+ dataSpecBytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void close() throws CacheDataSinkException {
+ if (dataSpec == null) {
+ return;
+ }
+ try {
+ closeCurrentOutputStream();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ private void openNextOutputStream() throws IOException {
+ long maxLength = dataSpec.length == C.LENGTH_UNSET ? maxCacheFileSize
+ : Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize);
+ file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten,
+ maxLength);
+ underlyingFileOutputStream = new FileOutputStream(file);
+ if (bufferSize > 0) {
+ if (bufferedOutputStream == null) {
+ bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream,
+ bufferSize);
+ } else {
+ bufferedOutputStream.reset(underlyingFileOutputStream);
+ }
+ outputStream = bufferedOutputStream;
+ } else {
+ outputStream = underlyingFileOutputStream;
+ }
+ outputStreamBytesWritten = 0;
+ }
+
+ @SuppressWarnings("ThrowFromFinallyBlock")
+ private void closeCurrentOutputStream() throws IOException {
+ if (outputStream == null) {
+ return;
+ }
+
+ boolean success = false;
+ try {
+ outputStream.flush();
+ underlyingFileOutputStream.getFD().sync();
+ success = true;
+ } finally {
+ Util.closeQuietly(outputStream);
+ outputStream = null;
+ File fileToCommit = file;
+ file = null;
+ if (success) {
+ cache.commitFile(fileToCommit);
+ } else {
+ fileToCommit.delete();
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.upstream.DataSink;
+
+/**
+ * A {@link DataSink.Factory} that produces {@link CacheDataSink}.
+ */
+public final class CacheDataSinkFactory implements DataSink.Factory {
+
+ private final Cache cache;
+ private final long maxCacheFileSize;
+ private final int bufferSize;
+
+ /**
+ * @see CacheDataSink#CacheDataSink(Cache, long)
+ */
+ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) {
+ this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * @see CacheDataSink#CacheDataSink(Cache, long, int)
+ */
+ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) {
+ this.cache = cache;
+ this.maxCacheFileSize = maxCacheFileSize;
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public DataSink createDataSink() {
+ return new CacheDataSink(cache, maxCacheFileSize, bufferSize);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -0,0 +1,383 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceException;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.FileDataSource;
+import com.google.android.exoplayer2.upstream.TeeDataSource;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache
+ * when possible. When data is not cached it is requested from an upstream {@link DataSource} and
+ * written into the cache.
+ */
+public final class CacheDataSource implements DataSource {
+
+ /**
+ * Default maximum single cache file size.
+ *
+ * @see #CacheDataSource(Cache, DataSource, int)
+ * @see #CacheDataSource(Cache, DataSource, int, long)
+ */
+ public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
+
+ /**
+ * Flags controlling the cache's behavior.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {FLAG_BLOCK_ON_CACHE, FLAG_IGNORE_CACHE_ON_ERROR,
+ FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS})
+ public @interface Flags {}
+ /**
+ * A flag indicating whether we will block reads if the cache key is locked. If this flag is
+ * set, then we will read from upstream if the cache key is locked.
+ */
+ public static final int FLAG_BLOCK_ON_CACHE = 1 << 0;
+
+ /**
+ * A flag indicating whether the cache is bypassed following any cache related error. If set
+ * then cache related exceptions may be thrown for one cycle of open, read and close calls.
+ * Subsequent cycles of these calls will then bypass the cache.
+ */
+ public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1;
+
+ /**
+ * A flag indicating that the cache should be bypassed for requests whose lengths are unset.
+ */
+ public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2;
+
+ /**
+ * Listener of {@link CacheDataSource} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Called when bytes have been read from the cache.
+ *
+ * @param cacheSizeBytes Current cache size in bytes.
+ * @param cachedBytesRead Total bytes read from the cache since this method was last called.
+ */
+ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
+
+ }
+
+ private final Cache cache;
+ private final DataSource cacheReadDataSource;
+ private final DataSource cacheWriteDataSource;
+ private final DataSource upstreamDataSource;
+ @Nullable private final EventListener eventListener;
+
+ private final boolean blockOnCache;
+ private final boolean ignoreCacheOnError;
+ private final boolean ignoreCacheForUnsetLengthRequests;
+
+ private DataSource currentDataSource;
+ private boolean currentRequestUnbounded;
+ private Uri uri;
+ private int flags;
+ private String key;
+ private long readPosition;
+ private long bytesRemaining;
+ private CacheSpan lockedSpan;
+ private boolean seenCacheError;
+ private boolean currentRequestIgnoresCache;
+ private long totalCachedBytesRead;
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {
+ this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE);
+ }
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache. The sink is configured to fragment data such that no single
+ * cache file is greater than maxCacheFileSize bytes.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
+ * #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
+ * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size
+ * exceeds this value, then the data will be fragmented into multiple cache files. The
+ * finer-grained this is the finer-grained the eviction policy can be.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags,
+ long maxCacheFileSize) {
+ this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize),
+ flags, null);
+ }
+
+ /**
+ * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache. One use of this constructor is to allow data to be transformed
+ * before it is written to disk.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
+ * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
+ * accessed read-only.
+ * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
+ * #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
+ * @param eventListener An optional {@link EventListener} to receive events.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
+ DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) {
+ this.cache = cache;
+ this.cacheReadDataSource = cacheReadDataSource;
+ this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0;
+ this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0;
+ this.ignoreCacheForUnsetLengthRequests =
+ (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;
+ this.upstreamDataSource = upstream;
+ if (cacheWriteDataSink != null) {
+ this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);
+ } else {
+ this.cacheWriteDataSource = null;
+ }
+ this.eventListener = eventListener;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ try {
+ uri = dataSpec.uri;
+ flags = dataSpec.flags;
+ key = CacheUtil.getKey(dataSpec);
+ readPosition = dataSpec.position;
+ currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError)
+ || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests);
+ if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
+ bytesRemaining = dataSpec.length;
+ } else {
+ bytesRemaining = cache.getContentLength(key);
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= dataSpec.position;
+ if (bytesRemaining <= 0) {
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ }
+ }
+ openNextSource(true);
+ return bytesRemaining;
+ } catch (IOException e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ try {
+ int bytesRead = currentDataSource.read(buffer, offset, readLength);
+ if (bytesRead >= 0) {
+ if (currentDataSource == cacheReadDataSource) {
+ totalCachedBytesRead += bytesRead;
+ }
+ readPosition += bytesRead;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ } else {
+ if (currentRequestUnbounded) {
+ // We only do unbounded requests to upstream and only when we don't know the actual stream
+ // length. So we reached the end of stream.
+ setContentLength(readPosition);
+ bytesRemaining = 0;
+ }
+ closeCurrentSource();
+ if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
+ if (openNextSource(false)) {
+ return read(buffer, offset, readLength);
+ }
+ }
+ }
+ return bytesRead;
+ } catch (IOException e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public Uri getUri() {
+ return currentDataSource == upstreamDataSource ? currentDataSource.getUri() : uri;
+ }
+
+ @Override
+ public void close() throws IOException {
+ uri = null;
+ notifyBytesRead();
+ try {
+ closeCurrentSource();
+ } catch (IOException e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ /**
+ * Opens the next source. If the cache contains data spanning the current read position then
+ * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is
+ * opened to read from the upstream source and write into the cache.
+ * @param initial Whether it is the initial open call.
+ */
+ private boolean openNextSource(boolean initial) throws IOException {
+ DataSpec dataSpec;
+ CacheSpan span;
+ if (currentRequestIgnoresCache) {
+ span = null;
+ } else if (blockOnCache) {
+ try {
+ span = cache.startReadWrite(key, readPosition);
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException();
+ }
+ } else {
+ span = cache.startReadWriteNonBlocking(key, readPosition);
+ }
+
+ if (span == null) {
+ // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
+ // from upstream.
+ currentDataSource = upstreamDataSource;
+ dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags);
+ } else if (span.isCached) {
+ // Data is cached, read from cache.
+ Uri fileUri = Uri.fromFile(span.file);
+ long filePosition = readPosition - span.position;
+ long length = span.length - filePosition;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ length = Math.min(length, bytesRemaining);
+ }
+ dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags);
+ currentDataSource = cacheReadDataSource;
+ } else {
+ // Data is not cached, and data is not locked, read from upstream with cache backing.
+ long length;
+ if (span.isOpenEnded()) {
+ length = bytesRemaining;
+ } else {
+ length = span.length;
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ length = Math.min(length, bytesRemaining);
+ }
+ }
+ dataSpec = new DataSpec(uri, readPosition, length, key, flags);
+ if (cacheWriteDataSource != null) {
+ currentDataSource = cacheWriteDataSource;
+ lockedSpan = span;
+ } else {
+ currentDataSource = upstreamDataSource;
+ cache.releaseHoleSpan(span);
+ }
+ }
+
+ currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET;
+ boolean successful = false;
+ long currentBytesRemaining = 0;
+ try {
+ currentBytesRemaining = currentDataSource.open(dataSpec);
+ successful = true;
+ } catch (IOException e) {
+ // if this isn't the initial open call (we had read some bytes) and an unbounded range request
+ // failed because of POSITION_OUT_OF_RANGE then mute the exception. We are trying to find the
+ // end of the stream.
+ if (!initial && currentRequestUnbounded) {
+ Throwable cause = e;
+ while (cause != null) {
+ if (cause instanceof DataSourceException) {
+ int reason = ((DataSourceException) cause).reason;
+ if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
+ e = null;
+ break;
+ }
+ }
+ cause = cause.getCause();
+ }
+ }
+ if (e != null) {
+ throw e;
+ }
+ }
+
+ // If we did an unbounded request (which means it's to upstream and
+ // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request
+ if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining = currentBytesRemaining;
+ setContentLength(dataSpec.position + bytesRemaining);
+ }
+ return successful;
+ }
+
+ private void setContentLength(long length) throws IOException {
+ // If writing into cache
+ if (currentDataSource == cacheWriteDataSource) {
+ cache.setContentLength(key, length);
+ }
+ }
+
+ private void closeCurrentSource() throws IOException {
+ if (currentDataSource == null) {
+ return;
+ }
+ try {
+ currentDataSource.close();
+ currentDataSource = null;
+ currentRequestUnbounded = false;
+ } finally {
+ if (lockedSpan != null) {
+ cache.releaseHoleSpan(lockedSpan);
+ lockedSpan = null;
+ }
+ }
+ }
+
+ private void handleBeforeThrow(IOException exception) {
+ if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) {
+ seenCacheError = true;
+ }
+ }
+
+ private void notifyBytesRead() {
+ if (eventListener != null && totalCachedBytesRead > 0) {
+ eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead);
+ totalCachedBytesRead = 0;
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSource.Factory;
+import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource.EventListener;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link CacheDataSource}.
+ */
+public final class CacheDataSourceFactory implements DataSource.Factory {
+
+ private final Cache cache;
+ private final DataSource.Factory upstreamFactory;
+ private final DataSource.Factory cacheReadDataSourceFactory;
+ private final DataSink.Factory cacheWriteDataSinkFactory;
+ private final int flags;
+ private final EventListener eventListener;
+
+ /**
+ * @see CacheDataSource#CacheDataSource(Cache, DataSource, int)
+ */
+ public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) {
+ this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
+ }
+
+ /**
+ * @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long)
+ */
+ public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags,
+ long maxCacheFileSize) {
+ this(cache, upstreamFactory, new FileDataSourceFactory(),
+ new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null);
+ }
+
+ /**
+ * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int,
+ * EventListener)
+ */
+ public CacheDataSourceFactory(Cache cache, Factory upstreamFactory,
+ Factory cacheReadDataSourceFactory,
+ DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) {
+ this.cache = cache;
+ this.upstreamFactory = upstreamFactory;
+ this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
+ this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
+ this.flags = flags;
+ this.eventListener = eventListener;
+ }
+
+ @Override
+ public CacheDataSource createDataSource() {
+ return new CacheDataSource(cache, upstreamFactory.createDataSource(),
+ cacheReadDataSourceFactory.createDataSource(),
+ cacheWriteDataSinkFactory != null ? cacheWriteDataSinkFactory.createDataSink() : null,
+ flags, eventListener);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
@@ -0,0 +1,39 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+/**
+ * Evicts data from a {@link Cache}. Implementations should call {@link Cache#removeSpan(CacheSpan)}
+ * to evict cache entries based on their eviction policies.
+ */
+public interface CacheEvictor extends Cache.Listener {
+
+ /**
+ * Called when cache has been initialized.
+ */
+ void onCacheInitialized();
+
+ /**
+ * Called when a writer starts writing to the cache.
+ *
+ * @param cache The source of the event.
+ * @param key The key being written.
+ * @param position The starting position of the data being written.
+ * @param maxLength The maximum length of the data being written.
+ */
+ void onStartFile(Cache cache, String key, long position, long maxLength);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
@@ -0,0 +1,107 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import android.support.annotation.NonNull;
+import com.google.android.exoplayer2.C;
+import java.io.File;
+
+/**
+ * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
+ */
+public class CacheSpan implements Comparable<CacheSpan> {
+
+ /**
+ * The cache key that uniquely identifies the original stream.
+ */
+ public final String key;
+ /**
+ * The position of the {@link CacheSpan} in the original stream.
+ */
+ public final long position;
+ /**
+ * The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an open-ended hole.
+ */
+ public final long length;
+ /**
+ * Whether the {@link CacheSpan} is cached.
+ */
+ public final boolean isCached;
+ /**
+ * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false.
+ */
+ public final File file;
+ /**
+ * The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false.
+ */
+ public final long lastAccessTimestamp;
+
+ /**
+ * Creates a hole CacheSpan which isn't cached, has no last access time and no file associated.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
+ * open-ended hole.
+ */
+ public CacheSpan(String key, long position, long length) {
+ this(key, position, length, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates a CacheSpan.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @param position The position of the {@link CacheSpan} in the original stream.
+ * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an
+ * open-ended hole.
+ * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if
+ * {@link #isCached} is false.
+ * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
+ */
+ public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) {
+ this.key = key;
+ this.position = position;
+ this.length = length;
+ this.isCached = file != null;
+ this.file = file;
+ this.lastAccessTimestamp = lastAccessTimestamp;
+ }
+
+ /**
+ * Returns whether this is an open-ended {@link CacheSpan}.
+ */
+ public boolean isOpenEnded() {
+ return length == C.LENGTH_UNSET;
+ }
+
+ /**
+ * Returns whether this is a hole {@link CacheSpan}.
+ */
+ public boolean isHoleSpan() {
+ return !isCached;
+ }
+
+ @Override
+ public int compareTo(@NonNull CacheSpan another) {
+ if (!key.equals(another.key)) {
+ return key.compareTo(another.key);
+ }
+ long startOffsetDiff = position - another.position;
+ return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -0,0 +1,237 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.NavigableSet;
+
+/**
+ * Caching related utility methods.
+ */
+public final class CacheUtil {
+
+ /** Holds the counters used during caching. */
+ public static class CachingCounters {
+ /** Total number of already cached bytes. */
+ public long alreadyCachedBytes;
+ /**
+ * Total number of downloaded bytes.
+ *
+ * <p>{@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing
+ * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't
+ * available in the {@code cache}.
+ */
+ public long downloadedBytes;
+ }
+
+ /**
+ * Generates a cache key out of the given {@link Uri}.
+ *
+ * @param uri Uri of a content which the requested key is for.
+ */
+ public static String generateKey(Uri uri) {
+ return uri.toString();
+ }
+
+ /**
+ * Returns the {@code dataSpec.key} if not null, otherwise generates a cache key out of {@code
+ * dataSpec.uri}
+ *
+ * @param dataSpec Defines a content which the requested key is for.
+ */
+ public static String getKey(DataSpec dataSpec) {
+ return dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri);
+ }
+
+ /**
+ * Returns already cached and missing bytes in the {@cache} for the data defined by {@code
+ * dataSpec}.
+ *
+ * @param dataSpec Defines the data to be checked.
+ * @param cache A {@link Cache} which has the data.
+ * @param counters The counters to be set. If null a new {@link CachingCounters} is created and
+ * used.
+ * @return The used {@link CachingCounters} instance.
+ */
+ public static CachingCounters getCached(DataSpec dataSpec, Cache cache,
+ CachingCounters counters) {
+ try {
+ return internalCache(dataSpec, cache, null, null, null, 0, counters);
+ } catch (IOException | InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Caches the data defined by {@code dataSpec} while skipping already cached data.
+ *
+ * @param dataSpec Defines the data to be cached.
+ * @param cache A {@link Cache} to store the data.
+ * @param dataSource A {@link CacheDataSource} that works on the {@code cache}.
+ * @param buffer The buffer to be used while caching.
+ * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
+ * caching.
+ * @param priority The priority of this task. Used with {@code priorityTaskManager}.
+ * @param counters The counters to be set during caching. If not null its values reset to
+ * zero before using. If null a new {@link CachingCounters} is created and used.
+ * @return The used {@link CachingCounters} instance.
+ * @throws IOException If an error occurs reading from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource,
+ byte[] buffer, PriorityTaskManager priorityTaskManager, int priority,
+ CachingCounters counters) throws IOException, InterruptedException {
+ Assertions.checkNotNull(dataSource);
+ Assertions.checkNotNull(buffer);
+ return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority,
+ counters);
+ }
+
+ /**
+ * Caches the data defined by {@code dataSpec} while skipping already cached data. If {@code
+ * dataSource} or {@code buffer} is null performs a dry run.
+ *
+ * @param dataSpec Defines the data to be cached.
+ * @param cache A {@link Cache} to store the data.
+ * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. If null a dry run
+ * is performed.
+ * @param buffer The buffer to be used while caching. If null a dry run is performed.
+ * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
+ * caching.
+ * @param priority The priority of this task. Used with {@code priorityTaskManager}.
+ * @param counters The counters to be set during caching. If not null its values reset to
+ * zero before using. If null a new {@link CachingCounters} is created and used.
+ * @return The used {@link CachingCounters} instance.
+ * @throws IOException If not dry run and an error occurs reading from the source.
+ * @throws InterruptedException If not dry run and the thread was interrupted.
+ */
+ private static CachingCounters internalCache(DataSpec dataSpec, Cache cache,
+ CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager,
+ int priority, CachingCounters counters) throws IOException, InterruptedException {
+ long start = dataSpec.position;
+ long left = dataSpec.length;
+ String key = getKey(dataSpec);
+ if (left == C.LENGTH_UNSET) {
+ left = cache.getContentLength(key);
+ if (left == C.LENGTH_UNSET) {
+ left = Long.MAX_VALUE;
+ }
+ }
+ if (counters == null) {
+ counters = new CachingCounters();
+ } else {
+ counters.alreadyCachedBytes = 0;
+ counters.downloadedBytes = 0;
+ }
+ while (left > 0) {
+ long blockLength = cache.getCachedBytes(key, start, left);
+ // Skip already cached data
+ if (blockLength > 0) {
+ counters.alreadyCachedBytes += blockLength;
+ } else {
+ // There is a hole in the cache which is at least "-blockLength" long.
+ blockLength = -blockLength;
+ if (dataSource != null && buffer != null) {
+ DataSpec subDataSpec = new DataSpec(dataSpec.uri, start,
+ blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key);
+ long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager,
+ priority);
+ counters.downloadedBytes += read;
+ if (read < blockLength) {
+ // Reached end of data.
+ break;
+ }
+ } else if (blockLength == Long.MAX_VALUE) {
+ counters.downloadedBytes = C.LENGTH_UNSET;
+ break;
+ } else {
+ counters.downloadedBytes += blockLength;
+ }
+ }
+ start += blockLength;
+ if (left != Long.MAX_VALUE) {
+ left -= blockLength;
+ }
+ }
+ return counters;
+ }
+
+ /**
+ * Reads and discards all data specified by the {@code dataSpec}.
+ *
+ * @param dataSpec Defines the data to be read.
+ * @param dataSource The {@link DataSource} to read the data from.
+ * @param buffer The buffer to be used while downloading.
+ * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
+ * caching.
+ * @param priority The priority of this task.
+ * @return Number of read bytes, or 0 if no data is available because the end of the opened range
+ * has been reached.
+ */
+ private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer,
+ PriorityTaskManager priorityTaskManager, int priority)
+ throws IOException, InterruptedException {
+ while (true) {
+ if (priorityTaskManager != null) {
+ // Wait for any other thread with higher priority to finish its job.
+ priorityTaskManager.proceed(priority);
+ }
+ try {
+ dataSource.open(dataSpec);
+ long totalRead = 0;
+ while (true) {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ int read = dataSource.read(buffer, 0, buffer.length);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return totalRead;
+ }
+ totalRead += read;
+ }
+ } catch (PriorityTaskManager.PriorityTooLowException exception) {
+ // catch and try again
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+ }
+
+ /** Removes all of the data in the {@code cache} pointed by the {@code key}. */
+ public static void remove(Cache cache, String key) {
+ NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(key);
+ if (cachedSpans == null) {
+ return;
+ }
+ for (CacheSpan cachedSpan : cachedSpans) {
+ try {
+ cache.removeSpan(cachedSpan);
+ } catch (Cache.CacheException e) {
+ // do nothing
+ }
+ }
+ }
+
+ private CacheUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
@@ -0,0 +1,199 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.TreeSet;
+
+/**
+ * Defines the cached content for a single stream.
+ */
+/*package*/ final class CachedContent {
+
+ /**
+ * The cache file id that uniquely identifies the original stream.
+ */
+ public final int id;
+ /**
+ * The cache key that uniquely identifies the original stream.
+ */
+ public final String key;
+ /**
+ * The cached spans of this content.
+ */
+ private final TreeSet<SimpleCacheSpan> cachedSpans;
+ /**
+ * The length of the original stream, or {@link C#LENGTH_UNSET} if the length is unknown.
+ */
+ private long length;
+
+ /**
+ * Reads an instance from a {@link DataInputStream}.
+ *
+ * @param input Input stream containing values needed to initialize CachedContent instance.
+ * @throws IOException If an error occurs during reading values.
+ */
+ public CachedContent(DataInputStream input) throws IOException {
+ this(input.readInt(), input.readUTF(), input.readLong());
+ }
+
+ /**
+ * Creates a CachedContent.
+ *
+ * @param id The cache file id.
+ * @param key The cache stream key.
+ * @param length The length of the original stream.
+ */
+ public CachedContent(int id, String key, long length) {
+ this.id = id;
+ this.key = key;
+ this.length = length;
+ this.cachedSpans = new TreeSet<>();
+ }
+
+ /**
+ * Writes the instance to a {@link DataOutputStream}.
+ *
+ * @param output Output stream to store the values.
+ * @throws IOException If an error occurs during writing values to output.
+ */
+ public void writeToStream(DataOutputStream output) throws IOException {
+ output.writeInt(id);
+ output.writeUTF(key);
+ output.writeLong(length);
+ }
+
+ /** Returns the length of the content. */
+ public long getLength() {
+ return length;
+ }
+
+ /** Sets the length of the content. */
+ public void setLength(long length) {
+ this.length = length;
+ }
+
+ /** Adds the given {@link SimpleCacheSpan} which contains a part of the content. */
+ public void addSpan(SimpleCacheSpan span) {
+ cachedSpans.add(span);
+ }
+
+ /** Returns a set of all {@link SimpleCacheSpan}s. */
+ public TreeSet<SimpleCacheSpan> getSpans() {
+ return cachedSpans;
+ }
+
+ /**
+ * Returns the span containing the position. If there isn't one, it returns a hole span
+ * which defines the maximum extents of the hole in the cache.
+ */
+ public SimpleCacheSpan getSpan(long position) {
+ SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position);
+ SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan);
+ if (floorSpan != null && floorSpan.position + floorSpan.length > position) {
+ return floorSpan;
+ }
+ SimpleCacheSpan ceilSpan = cachedSpans.ceiling(lookupSpan);
+ return ceilSpan == null ? SimpleCacheSpan.createOpenHole(key, position)
+ : SimpleCacheSpan.createClosedHole(key, position, ceilSpan.position - position);
+ }
+
+ /**
+ * Returns the length of the cached data block starting from the {@code position} to the block end
+ * up to {@code length} bytes. If the {@code position} isn't cached then -(the length of the gap
+ * to the next cached data up to {@code length} bytes) is returned.
+ *
+ * @param position The starting position of the data.
+ * @param length The maximum length of the data to be returned.
+ * @return the length of the cached or not cached data block length.
+ */
+ public long getCachedBytes(long position, long length) {
+ SimpleCacheSpan span = getSpan(position);
+ if (span.isHoleSpan()) {
+ // We don't have a span covering the start of the queried region.
+ return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length);
+ }
+ long queryEndPosition = position + length;
+ long currentEndPosition = span.position + span.length;
+ if (currentEndPosition < queryEndPosition) {
+ for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) {
+ if (next.position > currentEndPosition) {
+ // There's a hole in the cache within the queried region.
+ break;
+ }
+ // We expect currentEndPosition to always equal (next.position + next.length), but
+ // perform a max check anyway to guard against the existence of overlapping spans.
+ currentEndPosition = Math.max(currentEndPosition, next.position + next.length);
+ if (currentEndPosition >= queryEndPosition) {
+ // We've found spans covering the queried region.
+ break;
+ }
+ }
+ }
+ return Math.min(currentEndPosition - position, length);
+ }
+
+ /**
+ * Copies the given span with an updated last access time. Passed span becomes invalid after this
+ * call.
+ *
+ * @param cacheSpan Span to be copied and updated.
+ * @return a span with the updated last access time.
+ * @throws CacheException If renaming of the underlying span file failed.
+ */
+ public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException {
+ // Remove the old span from the in-memory representation.
+ Assertions.checkState(cachedSpans.remove(cacheSpan));
+ // Obtain a new span with updated last access timestamp.
+ SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id);
+ // Rename the cache file
+ if (!cacheSpan.file.renameTo(newCacheSpan.file)) {
+ throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file
+ + " failed.");
+ }
+ // Add the updated span back into the in-memory representation.
+ cachedSpans.add(newCacheSpan);
+ return newCacheSpan;
+ }
+
+ /** Returns whether there are any spans cached. */
+ public boolean isEmpty() {
+ return cachedSpans.isEmpty();
+ }
+
+ /** Removes the given span from cache. */
+ public boolean removeSpan(CacheSpan span) {
+ if (cachedSpans.remove(span)) {
+ span.file.delete();
+ return true;
+ }
+ return false;
+ }
+
+ /** Calculates a hash code for the header of this {@code CachedContent}. */
+ public int headerHashCode() {
+ int result = id;
+ result = 31 * result + key.hashCode();
+ result = 31 * result + (int) (length ^ (length >>> 32));
+ return result;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -0,0 +1,364 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import android.util.Log;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.AtomicFile;
+import com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
+import com.google.android.exoplayer2.util.Util;
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Random;
+import java.util.Set;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * This class maintains the index of cached content.
+ */
+/*package*/ final class CachedContentIndex {
+
+ public static final String FILE_NAME = "cached_content_index.exi";
+
+ private static final int VERSION = 1;
+
+ private static final int FLAG_ENCRYPTED_INDEX = 1;
+
+ private static final String TAG = "CachedContentIndex";
+
+ private final HashMap<String, CachedContent> keyToContent;
+ private final SparseArray<String> idToKey;
+ private final AtomicFile atomicFile;
+ private final Cipher cipher;
+ private final SecretKeySpec secretKeySpec;
+ private boolean changed;
+ private ReusableBufferedOutputStream bufferedOutputStream;
+
+ /**
+ * Creates a CachedContentIndex which works on the index file in the given cacheDir.
+ *
+ * @param cacheDir Directory where the index file is kept.
+ */
+ public CachedContentIndex(File cacheDir) {
+ this(cacheDir, null);
+ }
+
+ /**
+ * Creates a CachedContentIndex which works on the index file in the given cacheDir.
+ *
+ * @param cacheDir Directory where the index file is kept.
+ * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+ * The key must be 16 bytes long.
+ */
+ public CachedContentIndex(File cacheDir, byte[] secretKey) {
+ if (secretKey != null) {
+ Assertions.checkArgument(secretKey.length == 16);
+ try {
+ cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
+ secretKeySpec = new SecretKeySpec(secretKey, "AES");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+ } else {
+ cipher = null;
+ secretKeySpec = null;
+ }
+ keyToContent = new HashMap<>();
+ idToKey = new SparseArray<>();
+ atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
+ }
+
+ /** Loads the index file. */
+ public void load() {
+ Assertions.checkState(!changed);
+ if (!readFile()) {
+ atomicFile.delete();
+ keyToContent.clear();
+ idToKey.clear();
+ }
+ }
+
+ /** Stores the index data to index file if there is a change. */
+ public void store() throws CacheException {
+ if (!changed) {
+ return;
+ }
+ writeFile();
+ changed = false;
+ }
+
+ /**
+ * Adds the given key to the index if it isn't there already.
+ *
+ * @param key The cache key that uniquely identifies the original stream.
+ * @return A new or existing CachedContent instance with the given key.
+ */
+ public CachedContent add(String key) {
+ CachedContent cachedContent = keyToContent.get(key);
+ if (cachedContent == null) {
+ cachedContent = addNew(key, C.LENGTH_UNSET);
+ }
+ return cachedContent;
+ }
+
+ /** Returns a CachedContent instance with the given key or null if there isn't one. */
+ public CachedContent get(String key) {
+ return keyToContent.get(key);
+ }
+
+ /**
+ * Returns a Collection of all CachedContent instances in the index. The collection is backed by
+ * the {@code keyToContent} map, so changes to the map are reflected in the collection, and
+ * vice-versa. If the map is modified while an iteration over the collection is in progress
+ * (except through the iterator's own remove operation), the results of the iteration are
+ * undefined.
+ */
+ public Collection<CachedContent> getAll() {
+ return keyToContent.values();
+ }
+
+ /** Returns an existing or new id assigned to the given key. */
+ public int assignIdForKey(String key) {
+ return add(key).id;
+ }
+
+ /** Returns the key which has the given id assigned. */
+ public String getKeyForId(int id) {
+ return idToKey.get(id);
+ }
+
+ /**
+ * Removes {@link CachedContent} with the given key from index. It shouldn't contain any spans.
+ *
+ * @throws IllegalStateException If {@link CachedContent} isn't empty.
+ */
+ public void removeEmpty(String key) {
+ CachedContent cachedContent = keyToContent.remove(key);
+ if (cachedContent != null) {
+ Assertions.checkState(cachedContent.isEmpty());
+ idToKey.remove(cachedContent.id);
+ changed = true;
+ }
+ }
+
+ /** Removes empty {@link CachedContent} instances from index. */
+ public void removeEmpty() {
+ LinkedList<String> cachedContentToBeRemoved = new LinkedList<>();
+ for (CachedContent cachedContent : keyToContent.values()) {
+ if (cachedContent.isEmpty()) {
+ cachedContentToBeRemoved.add(cachedContent.key);
+ }
+ }
+ for (String key : cachedContentToBeRemoved) {
+ removeEmpty(key);
+ }
+ }
+
+ /**
+ * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so
+ * changes to the map are reflected in the set, and vice-versa. If the map is modified while an
+ * iteration over the set is in progress (except through the iterator's own remove operation), the
+ * results of the iteration are undefined.
+ */
+ public Set<String> getKeys() {
+ return keyToContent.keySet();
+ }
+
+ /**
+ * Sets the content length for the given key. A new {@link CachedContent} is added if there isn't
+ * one already with the given key.
+ */
+ public void setContentLength(String key, long length) {
+ CachedContent cachedContent = get(key);
+ if (cachedContent != null) {
+ if (cachedContent.getLength() != length) {
+ cachedContent.setLength(length);
+ changed = true;
+ }
+ } else {
+ addNew(key, length);
+ }
+ }
+
+ /**
+ * Returns the content length for the given key if one set, or {@link
+ * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
+ */
+ public long getContentLength(String key) {
+ CachedContent cachedContent = get(key);
+ return cachedContent == null ? C.LENGTH_UNSET : cachedContent.getLength();
+ }
+
+ private boolean readFile() {
+ DataInputStream input = null;
+ try {
+ InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
+ input = new DataInputStream(inputStream);
+ int version = input.readInt();
+ if (version != VERSION) {
+ // Currently there is no other version
+ return false;
+ }
+
+ int flags = input.readInt();
+ if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {
+ if (cipher == null) {
+ return false;
+ }
+ byte[] initializationVector = new byte[16];
+ input.readFully(initializationVector);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalStateException(e);
+ }
+ input = new DataInputStream(new CipherInputStream(inputStream, cipher));
+ } else {
+ if (cipher != null) {
+ changed = true; // Force index to be rewritten encrypted after read.
+ }
+ }
+
+ int count = input.readInt();
+ int hashCode = 0;
+ for (int i = 0; i < count; i++) {
+ CachedContent cachedContent = new CachedContent(input);
+ add(cachedContent);
+ hashCode += cachedContent.headerHashCode();
+ }
+ if (input.readInt() != hashCode) {
+ return false;
+ }
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading cache content index file.", e);
+ return false;
+ } finally {
+ if (input != null) {
+ Util.closeQuietly(input);
+ }
+ }
+ return true;
+ }
+
+ private void writeFile() throws CacheException {
+ DataOutputStream output = null;
+ try {
+ OutputStream outputStream = atomicFile.startWrite();
+ if (bufferedOutputStream == null) {
+ bufferedOutputStream = new ReusableBufferedOutputStream(outputStream);
+ } else {
+ bufferedOutputStream.reset(outputStream);
+ }
+ output = new DataOutputStream(bufferedOutputStream);
+ output.writeInt(VERSION);
+
+ int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0;
+ output.writeInt(flags);
+
+ if (cipher != null) {
+ byte[] initializationVector = new byte[16];
+ new Random().nextBytes(initializationVector);
+ output.write(initializationVector);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+ output.flush();
+ output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher));
+ }
+
+ output.writeInt(keyToContent.size());
+ int hashCode = 0;
+ for (CachedContent cachedContent : keyToContent.values()) {
+ cachedContent.writeToStream(output);
+ hashCode += cachedContent.headerHashCode();
+ }
+ output.writeInt(hashCode);
+ atomicFile.endWrite(output);
+ // Avoid calling close twice. Duplicate CipherOutputStream.close calls did
+ // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/
+ output = null;
+ } catch (IOException e) {
+ throw new CacheException(e);
+ } finally {
+ Util.closeQuietly(output);
+ }
+ }
+
+ private void add(CachedContent cachedContent) {
+ keyToContent.put(cachedContent.key, cachedContent);
+ idToKey.put(cachedContent.id, cachedContent.key);
+ }
+
+ /** Adds the given CachedContent to the index. */
+ /*package*/ void addNew(CachedContent cachedContent) {
+ add(cachedContent);
+ changed = true;
+ }
+
+ private CachedContent addNew(String key, long length) {
+ int id = getNewId(idToKey);
+ CachedContent cachedContent = new CachedContent(id, key, length);
+ addNew(cachedContent);
+ return cachedContent;
+ }
+
+ /**
+ * Returns an id which isn't used in the given array. If the maximum id in the array is smaller
+ * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it
+ * returns the smallest unused non-negative integer.
+ */
+ //@VisibleForTesting
+ public static int getNewId(SparseArray<String> idToKey) {
+ int size = idToKey.size();
+ int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1);
+ if (id < 0) { // In case if we pass max int value.
+ // TODO optimization: defragmentation or binary search?
+ for (id = 0; id < size; id++) {
+ if (id != idToKey.keyAt(id)) {
+ break;
+ }
+ }
+ }
+ return id;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
@@ -0,0 +1,206 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NavigableSet;
+import java.util.TreeSet;
+
+/**
+ * Utility class for efficiently tracking regions of data that are stored in a {@link Cache}
+ * for a given cache key.
+ */
+public final class CachedRegionTracker implements Cache.Listener {
+
+ private static final String TAG = "CachedRegionTracker";
+
+ public static final int NOT_CACHED = -1;
+ public static final int CACHED_TO_END = -2;
+
+ private final Cache cache;
+ private final String cacheKey;
+ private final ChunkIndex chunkIndex;
+
+ private final TreeSet<Region> regions;
+ private final Region lookupRegion;
+
+ public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) {
+ this.cache = cache;
+ this.cacheKey = cacheKey;
+ this.chunkIndex = chunkIndex;
+ this.regions = new TreeSet<>();
+ this.lookupRegion = new Region(0, 0);
+
+ synchronized (this) {
+ NavigableSet<CacheSpan> cacheSpans = cache.addListener(cacheKey, this);
+ if (cacheSpans != null) {
+ // Merge the spans into regions. mergeSpan is more efficient when merging from high to low,
+ // which is why a descending iterator is used here.
+ Iterator<CacheSpan> spanIterator = cacheSpans.descendingIterator();
+ while (spanIterator.hasNext()) {
+ CacheSpan span = spanIterator.next();
+ mergeSpan(span);
+ }
+ }
+ }
+ }
+
+ public void release() {
+ cache.removeListener(cacheKey, this);
+ }
+
+ /**
+ * When provided with a byte offset, this method locates the cached region within which the
+ * offset falls, and returns the approximate end position in milliseconds of that region. If the
+ * byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned.
+ * If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned.
+ *
+ * @param byteOffset The byte offset in the underlying stream.
+ * @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or
+ * {@link #CACHED_TO_END}.
+ */
+ public synchronized int getRegionEndTimeMs(long byteOffset) {
+ lookupRegion.startOffset = byteOffset;
+ Region floorRegion = regions.floor(lookupRegion);
+ if (floorRegion == null || byteOffset > floorRegion.endOffset
+ || floorRegion.endOffsetIndex == -1) {
+ return NOT_CACHED;
+ }
+ int index = floorRegion.endOffsetIndex;
+ if (index == chunkIndex.length - 1
+ && floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) {
+ return CACHED_TO_END;
+ }
+ long segmentFractionUs = (chunkIndex.durationsUs[index]
+ * (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index];
+ return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000);
+ }
+
+ @Override
+ public synchronized void onSpanAdded(Cache cache, CacheSpan span) {
+ mergeSpan(span);
+ }
+
+ @Override
+ public synchronized void onSpanRemoved(Cache cache, CacheSpan span) {
+ Region removedRegion = new Region(span.position, span.position + span.length);
+
+ // Look up a region this span falls into.
+ Region floorRegion = regions.floor(removedRegion);
+ if (floorRegion == null) {
+ Log.e(TAG, "Removed a span we were not aware of");
+ return;
+ }
+
+ // Remove it.
+ regions.remove(floorRegion);
+
+ // Add new floor and ceiling regions, if necessary.
+ if (floorRegion.startOffset < removedRegion.startOffset) {
+ Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset);
+
+ int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset);
+ newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
+ regions.add(newFloorRegion);
+ }
+
+ if (floorRegion.endOffset > removedRegion.endOffset) {
+ Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset);
+ newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex;
+ regions.add(newCeilingRegion);
+ }
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+ private void mergeSpan(CacheSpan span) {
+ Region newRegion = new Region(span.position, span.position + span.length);
+ Region floorRegion = regions.floor(newRegion);
+ Region ceilingRegion = regions.ceiling(newRegion);
+ boolean floorConnects = regionsConnect(floorRegion, newRegion);
+ boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion);
+
+ if (ceilingConnects) {
+ if (floorConnects) {
+ // Extend floorRegion to cover both newRegion and ceilingRegion.
+ floorRegion.endOffset = ceilingRegion.endOffset;
+ floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
+ } else {
+ // Extend newRegion to cover ceilingRegion. Add it.
+ newRegion.endOffset = ceilingRegion.endOffset;
+ newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex;
+ regions.add(newRegion);
+ }
+ regions.remove(ceilingRegion);
+ } else if (floorConnects) {
+ // Extend floorRegion to the right to cover newRegion.
+ floorRegion.endOffset = newRegion.endOffset;
+ int index = floorRegion.endOffsetIndex;
+ while (index < chunkIndex.length - 1
+ && (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) {
+ index++;
+ }
+ floorRegion.endOffsetIndex = index;
+ } else {
+ // This is a new region.
+ int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset);
+ newRegion.endOffsetIndex = index < 0 ? -index - 2 : index;
+ regions.add(newRegion);
+ }
+ }
+
+ private boolean regionsConnect(Region lower, Region upper) {
+ return lower != null && upper != null && lower.endOffset == upper.startOffset;
+ }
+
+ private static class Region implements Comparable<Region> {
+
+ /**
+ * The first byte of the region (inclusive).
+ */
+ public long startOffset;
+ /**
+ * End offset of the region (exclusive).
+ */
+ public long endOffset;
+ /**
+ * The index in chunkIndex that contains the end offset. May be -1 if the end offset comes
+ * before the start of the first media chunk (i.e. if the end offset is within the stream
+ * header).
+ */
+ public int endOffsetIndex;
+
+ public Region(long position, long endOffset) {
+ this.startOffset = position;
+ this.endOffset = endOffset;
+ }
+
+ @Override
+ public int compareTo(@NonNull Region another) {
+ return startOffset < another.startOffset ? -1
+ : startOffset == another.startOffset ? 0 : 1;
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * Evicts least recently used cache files first.
+ */
+public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator<CacheSpan> {
+
+ private final long maxBytes;
+ private final TreeSet<CacheSpan> leastRecentlyUsed;
+
+ private long currentSize;
+
+ public LeastRecentlyUsedCacheEvictor(long maxBytes) {
+ this.maxBytes = maxBytes;
+ this.leastRecentlyUsed = new TreeSet<>(this);
+ }
+
+ @Override
+ public void onCacheInitialized() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long maxLength) {
+ evictCache(cache, maxLength);
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.add(span);
+ currentSize += span.length;
+ evictCache(cache, 0);
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.remove(span);
+ currentSize -= span.length;
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ onSpanRemoved(cache, oldSpan);
+ onSpanAdded(cache, newSpan);
+ }
+
+ @Override
+ public int compare(CacheSpan lhs, CacheSpan rhs) {
+ long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp;
+ if (lastAccessTimestampDelta == 0) {
+ // Use the standard compareTo method as a tie-break.
+ return lhs.compareTo(rhs);
+ }
+ return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1;
+ }
+
+ private void evictCache(Cache cache, long requiredSpace) {
+ while (currentSize + requiredSpace > maxBytes) {
+ try {
+ cache.removeSpan(leastRecentlyUsed.first());
+ } catch (CacheException e) {
+ // do nothing.
+ }
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java
@@ -0,0 +1,52 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+
+/**
+ * Evictor that doesn't ever evict cache files.
+ *
+ * Warning: Using this evictor might have unforeseeable consequences if cache
+ * size is not managed elsewhere.
+ */
+public final class NoOpCacheEvictor implements CacheEvictor {
+
+ @Override
+ public void onCacheInitialized() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long maxLength) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
@@ -0,0 +1,377 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import android.os.ConditionVariable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * A {@link Cache} implementation that maintains an in-memory representation.
+ */
+public final class SimpleCache implements Cache {
+
+ private final File cacheDir;
+ private final CacheEvictor evictor;
+ private final HashMap<String, CacheSpan> lockedSpans;
+ private final CachedContentIndex index;
+ private final HashMap<String, ArrayList<Listener>> listeners;
+ private long totalSpace = 0;
+ private CacheException initializationException;
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used.
+ */
+ public SimpleCache(File cacheDir, CacheEvictor evictor) {
+ this(cacheDir, evictor, null);
+ }
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ * @param evictor The evictor to be used.
+ * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+ * The key must be 16 bytes long.
+ */
+ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) {
+ this.cacheDir = cacheDir;
+ this.evictor = evictor;
+ this.lockedSpans = new HashMap<>();
+ this.index = new CachedContentIndex(cacheDir, secretKey);
+ this.listeners = new HashMap<>();
+ // Start cache initialization.
+ final ConditionVariable conditionVariable = new ConditionVariable();
+ new Thread("SimpleCache.initialize()") {
+ @Override
+ public void run() {
+ synchronized (SimpleCache.this) {
+ conditionVariable.open();
+ try {
+ initialize();
+ } catch (CacheException e) {
+ initializationException = e;
+ }
+ SimpleCache.this.evictor.onCacheInitialized();
+ }
+ }
+ }.start();
+ conditionVariable.block();
+ }
+
+ @Override
+ public synchronized NavigableSet<CacheSpan> addListener(String key, Listener listener) {
+ ArrayList<Listener> listenersForKey = listeners.get(key);
+ if (listenersForKey == null) {
+ listenersForKey = new ArrayList<>();
+ listeners.put(key, listenersForKey);
+ }
+ listenersForKey.add(listener);
+ return getCachedSpans(key);
+ }
+
+ @Override
+ public synchronized void removeListener(String key, Listener listener) {
+ ArrayList<Listener> listenersForKey = listeners.get(key);
+ if (listenersForKey != null) {
+ listenersForKey.remove(listener);
+ if (listenersForKey.isEmpty()) {
+ listeners.remove(key);
+ }
+ }
+ }
+
+ @Override
+ public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {
+ CachedContent cachedContent = index.get(key);
+ return cachedContent == null ? null : new TreeSet<CacheSpan>(cachedContent.getSpans());
+ }
+
+ @Override
+ public synchronized Set<String> getKeys() {
+ return new HashSet<>(index.getKeys());
+ }
+
+ @Override
+ public synchronized long getCacheSpace() {
+ return totalSpace;
+ }
+
+ @Override
+ public synchronized SimpleCacheSpan startReadWrite(String key, long position)
+ throws InterruptedException, CacheException {
+ while (true) {
+ SimpleCacheSpan span = startReadWriteNonBlocking(key, position);
+ if (span != null) {
+ return span;
+ } else {
+ // Write case, lock not available. We'll be woken up when a locked span is released (if the
+ // released lock is for the requested key then we'll be able to make progress) or when a
+ // span is added to the cache (if the span is for the requested key and covers the requested
+ // position, then we'll become a read and be able to make progress).
+ wait();
+ }
+ }
+ }
+
+ @Override
+ public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
+ throws CacheException {
+ if (initializationException != null) {
+ throw initializationException;
+ }
+
+ SimpleCacheSpan cacheSpan = getSpan(key, position);
+
+ // Read case.
+ if (cacheSpan.isCached) {
+ // Obtain a new span with updated last access timestamp.
+ SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan);
+ notifySpanTouched(cacheSpan, newCacheSpan);
+ return newCacheSpan;
+ }
+
+ // Write case, lock available.
+ if (!lockedSpans.containsKey(key)) {
+ lockedSpans.put(key, cacheSpan);
+ return cacheSpan;
+ }
+
+ // Write case, lock not available.
+ return null;
+ }
+
+ @Override
+ public synchronized File startFile(String key, long position, long maxLength)
+ throws CacheException {
+ Assertions.checkState(lockedSpans.containsKey(key));
+ if (!cacheDir.exists()) {
+ // For some reason the cache directory doesn't exist. Make a best effort to create it.
+ removeStaleSpansAndCachedContents();
+ cacheDir.mkdirs();
+ }
+ evictor.onStartFile(this, key, position, maxLength);
+ return SimpleCacheSpan.getCacheFile(cacheDir, index.assignIdForKey(key), position,
+ System.currentTimeMillis());
+ }
+
+ @Override
+ public synchronized void commitFile(File file) throws CacheException {
+ SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index);
+ Assertions.checkState(span != null);
+ Assertions.checkState(lockedSpans.containsKey(span.key));
+ // If the file doesn't exist, don't add it to the in-memory representation.
+ if (!file.exists()) {
+ return;
+ }
+ // If the file has length 0, delete it and don't add it to the in-memory representation.
+ if (file.length() == 0) {
+ file.delete();
+ return;
+ }
+ // Check if the span conflicts with the set content length
+ Long length = getContentLength(span.key);
+ if (length != C.LENGTH_UNSET) {
+ Assertions.checkState((span.position + span.length) <= length);
+ }
+ addSpan(span);
+ index.store();
+ notifyAll();
+ }
+
+ @Override
+ public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
+ Assertions.checkState(holeSpan == lockedSpans.remove(holeSpan.key));
+ notifyAll();
+ }
+
+ /**
+ * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link
+ * SimpleCacheSpan}.
+ *
+ * <p>If the lookup position is contained by an existing entry in the cache, then the returned
+ * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is
+ * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the
+ * maximum extents of the hole in the cache.
+ *
+ * @param key The key of the span being requested.
+ * @param position The position of the span being requested.
+ * @return The corresponding cache {@link SimpleCacheSpan}.
+ */
+ private SimpleCacheSpan getSpan(String key, long position) throws CacheException {
+ CachedContent cachedContent = index.get(key);
+ if (cachedContent == null) {
+ return SimpleCacheSpan.createOpenHole(key, position);
+ }
+ while (true) {
+ SimpleCacheSpan span = cachedContent.getSpan(position);
+ if (span.isCached && !span.file.exists()) {
+ // The file has been deleted from under us. It's likely that other files will have been
+ // deleted too, so scan the whole in-memory representation.
+ removeStaleSpansAndCachedContents();
+ continue;
+ }
+ return span;
+ }
+ }
+
+ /**
+ * Ensures that the cache's in-memory representation has been initialized.
+ */
+ private void initialize() throws CacheException {
+ if (!cacheDir.exists()) {
+ cacheDir.mkdirs();
+ return;
+ }
+
+ index.load();
+
+ File[] files = cacheDir.listFiles();
+ if (files == null) {
+ return;
+ }
+ for (File file : files) {
+ if (file.getName().equals(CachedContentIndex.FILE_NAME)) {
+ continue;
+ }
+ SimpleCacheSpan span = file.length() > 0
+ ? SimpleCacheSpan.createCacheEntry(file, index) : null;
+ if (span != null) {
+ addSpan(span);
+ } else {
+ file.delete();
+ }
+ }
+
+ index.removeEmpty();
+ index.store();
+ }
+
+ /**
+ * Adds a cached span to the in-memory representation.
+ *
+ * @param span The span to be added.
+ */
+ private void addSpan(SimpleCacheSpan span) {
+ index.add(span.key).addSpan(span);
+ totalSpace += span.length;
+ notifySpanAdded(span);
+ }
+
+ private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
+ CachedContent cachedContent = index.get(span.key);
+ Assertions.checkState(cachedContent.removeSpan(span));
+ totalSpace -= span.length;
+ if (removeEmptyCachedContent && cachedContent.isEmpty()) {
+ index.removeEmpty(cachedContent.key);
+ index.store();
+ }
+ notifySpanRemoved(span);
+ }
+
+ @Override
+ public synchronized void removeSpan(CacheSpan span) throws CacheException {
+ removeSpan(span, true);
+ }
+
+ /**
+ * Scans all of the cached spans in the in-memory representation, removing any for which files
+ * no longer exist.
+ */
+ private void removeStaleSpansAndCachedContents() throws CacheException {
+ LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>();
+ for (CachedContent cachedContent : index.getAll()) {
+ for (CacheSpan span : cachedContent.getSpans()) {
+ if (!span.file.exists()) {
+ spansToBeRemoved.add(span);
+ }
+ }
+ }
+ for (CacheSpan span : spansToBeRemoved) {
+ // Remove span but not CachedContent to prevent multiple index.store() calls.
+ removeSpan(span, false);
+ }
+ index.removeEmpty();
+ index.store();
+ }
+
+ private void notifySpanRemoved(CacheSpan span) {
+ ArrayList<Listener> keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanRemoved(this, span);
+ }
+ }
+ evictor.onSpanRemoved(this, span);
+ }
+
+ private void notifySpanAdded(SimpleCacheSpan span) {
+ ArrayList<Listener> keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanAdded(this, span);
+ }
+ }
+ evictor.onSpanAdded(this, span);
+ }
+
+ private void notifySpanTouched(SimpleCacheSpan oldSpan, CacheSpan newSpan) {
+ ArrayList<Listener> keyListeners = listeners.get(oldSpan.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan);
+ }
+ }
+ evictor.onSpanTouched(this, oldSpan, newSpan);
+ }
+
+ @Override
+ public synchronized boolean isCached(String key, long position, long length) {
+ CachedContent cachedContent = index.get(key);
+ return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length;
+ }
+
+ @Override
+ public synchronized long getCachedBytes(String key, long position, long length) {
+ CachedContent cachedContent = index.get(key);
+ return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length;
+ }
+
+ @Override
+ public synchronized void setContentLength(String key, long length) throws CacheException {
+ index.setContentLength(key, length);
+ index.store();
+ }
+
+ @Override
+ public synchronized long getContentLength(String key) {
+ return index.getContentLength(key);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.File;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class stores span metadata in filename.
+ */
+/*package*/ final class SimpleCacheSpan extends CacheSpan {
+
+ private static final String SUFFIX = ".v3.exo";
+ private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile(
+ "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL);
+ private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile(
+ "^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$", Pattern.DOTALL);
+ private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile(
+ "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL);
+
+ public static File getCacheFile(File cacheDir, int id, long position,
+ long lastAccessTimestamp) {
+ return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX);
+ }
+
+ public static SimpleCacheSpan createLookup(String key, long position) {
+ return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
+ }
+
+ public static SimpleCacheSpan createOpenHole(String key, long position) {
+ return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
+ }
+
+ public static SimpleCacheSpan createClosedHole(String key, long position, long length) {
+ return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null);
+ }
+
+ /**
+ * Creates a cache span from an underlying cache file. Upgrades the file if necessary.
+ *
+ * @param file The cache file.
+ * @param index Cached content index.
+ * @return The span, or null if the file name is not correctly formatted, or if the id is not
+ * present in the content index.
+ */
+ public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) {
+ String name = file.getName();
+ if (!name.endsWith(SUFFIX)) {
+ file = upgradeFile(file, index);
+ if (file == null) {
+ return null;
+ }
+ name = file.getName();
+ }
+
+ Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name);
+ if (!matcher.matches()) {
+ return null;
+ }
+ long length = file.length();
+ int id = Integer.parseInt(matcher.group(1));
+ String key = index.getKeyForId(id);
+ return key == null ? null : new SimpleCacheSpan(key, Long.parseLong(matcher.group(2)), length,
+ Long.parseLong(matcher.group(3)), file);
+ }
+
+ private static File upgradeFile(File file, CachedContentIndex index) {
+ String key;
+ String filename = file.getName();
+ Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);
+ if (matcher.matches()) {
+ key = Util.unescapeFileName(matcher.group(1));
+ if (key == null) {
+ return null;
+ }
+ } else {
+ matcher = CACHE_FILE_PATTERN_V1.matcher(filename);
+ if (!matcher.matches()) {
+ return null;
+ }
+ key = matcher.group(1); // Keys were not escaped in version 1.
+ }
+
+ File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key),
+ Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)));
+ if (!file.renameTo(newCacheFile)) {
+ return null;
+ }
+ return newCacheFile;
+ }
+
+ private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp,
+ File file) {
+ super(key, position, length, lastAccessTimestamp, file);
+ }
+
+ /**
+ * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This
+ * doesn't copy or change the underlying cache file.
+ *
+ * @param id The cache file id.
+ * @return A {@link SimpleCacheSpan} with updated last access time stamp.
+ * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false).
+ */
+ public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) {
+ Assertions.checkState(isCached);
+ long now = System.currentTimeMillis();
+ File newCacheFile = getCacheFile(file.getParentFile(), id, position, now);
+ return new SimpleCacheSpan(key, position, length, now, newCacheFile);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
@@ -0,0 +1,95 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.crypto;
+
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+import javax.crypto.Cipher;
+
+/**
+ * A wrapping {@link DataSink} that encrypts the data being consumed.
+ */
+public final class AesCipherDataSink implements DataSink {
+
+ private final DataSink wrappedDataSink;
+ private final byte[] secretKey;
+ private final byte[] scratch;
+
+ private AesFlushingCipher cipher;
+
+ /**
+ * Create an instance whose {@code write} methods have the side effect of overwriting the input
+ * {@code data}. Use this constructor for maximum efficiency in the case that there is no
+ * requirement for the input data arrays to remain unchanged.
+ *
+ * @param secretKey The key data.
+ * @param wrappedDataSink The wrapped {@link DataSink}.
+ */
+ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) {
+ this(secretKey, wrappedDataSink, null);
+ }
+
+ /**
+ * Create an instance whose {@code write} methods are free of side effects. Use this constructor
+ * when the input data arrays are required to remain unchanged.
+ *
+ * @param secretKey The key data.
+ * @param wrappedDataSink The wrapped {@link DataSink}.
+ * @param scratch Scratch space. Data is decrypted into this array before being written to the
+ * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a
+ * write is larger than the size of this array the write will still succeed, but multiple
+ * cipher calls will be required to complete the operation.
+ */
+ public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) {
+ this.wrappedDataSink = wrappedDataSink;
+ this.secretKey = secretKey;
+ this.scratch = scratch;
+ }
+
+ @Override
+ public void open(DataSpec dataSpec) throws IOException {
+ wrappedDataSink.open(dataSpec);
+ long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
+ cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce,
+ dataSpec.absoluteStreamPosition);
+ }
+
+ @Override
+ public void write(byte[] data, int offset, int length) throws IOException {
+ if (scratch == null) {
+ // In-place mode. Writes over the input data.
+ cipher.updateInPlace(data, offset, length);
+ wrappedDataSink.write(data, offset, length);
+ } else {
+ // Use scratch space. The original data remains intact.
+ int bytesProcessed = 0;
+ while (bytesProcessed < length) {
+ int bytesToProcess = Math.min(length - bytesProcessed, scratch.length);
+ cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0);
+ wrappedDataSink.write(scratch, 0, bytesToProcess);
+ bytesProcessed += bytesToProcess;
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipher = null;
+ wrappedDataSink.close();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.crypto;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+import javax.crypto.Cipher;
+
+/**
+ * A {@link DataSource} that decrypts the data read from an upstream source.
+ */
+public final class AesCipherDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final byte[] secretKey;
+
+ private AesFlushingCipher cipher;
+
+ public AesCipherDataSource(byte[] secretKey, DataSource upstream) {
+ this.upstream = upstream;
+ this.secretKey = secretKey;
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ long dataLength = upstream.open(dataSpec);
+ long nonce = CryptoUtil.getFNV64Hash(dataSpec.key);
+ cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce,
+ dataSpec.absoluteStreamPosition);
+ return dataLength;
+ }
+
+ @Override
+ public int read(byte[] data, int offset, int readLength) throws IOException {
+ if (readLength == 0) {
+ return 0;
+ }
+ int read = upstream.read(data, offset, readLength);
+ if (read == C.RESULT_END_OF_INPUT) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ cipher.updateInPlace(data, offset, read);
+ return read;
+ }
+
+ @Override
+ public void close() throws IOException {
+ cipher = null;
+ upstream.close();
+ }
+
+ @Override
+ public Uri getUri() {
+ return upstream.getUri();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
@@ -0,0 +1,120 @@
+/*
+ * 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 com.google.android.exoplayer2.upstream.crypto;
+
+import com.google.android.exoplayer2.util.Assertions;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * A flushing variant of a AES/CTR/NoPadding {@link Cipher}.
+ *
+ * Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all
+ * of the bytes input (and hence output the same number of bytes).
+ */
+public final class AesFlushingCipher {
+
+ private final Cipher cipher;
+ private final int blockSize;
+ private final byte[] zerosBlock;
+ private final byte[] flushedBlock;
+
+ private int pendingXorBytes;
+
+ public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) {
+ try {
+ cipher = Cipher.getInstance("AES/CTR/NoPadding");
+ blockSize = cipher.getBlockSize();
+ zerosBlock = new byte[blockSize];
+ flushedBlock = new byte[blockSize];
+ long counter = offset / blockSize;
+ int startPadding = (int) (offset % blockSize);
+ cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]),
+ new IvParameterSpec(getInitializationVector(nonce, counter)));
+ if (startPadding != 0) {
+ updateInPlace(new byte[startPadding], 0, startPadding);
+ }
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | InvalidAlgorithmParameterException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void updateInPlace(byte[] data, int offset, int length) {
+ update(data, offset, length, data, offset);
+ }
+
+ public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
+ // If we previously flushed the cipher by inputting zeros up to a block boundary, then we need
+ // to manually transform the data that actually ended the block. See the comment below for more
+ // details.
+ while (pendingXorBytes > 0) {
+ out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]);
+ outOffset++;
+ inOffset++;
+ pendingXorBytes--;
+ length--;
+ if (length == 0) {
+ return;
+ }
+ }
+
+ // Do the bulk of the update.
+ int written = nonFlushingUpdate(in, inOffset, length, out, outOffset);
+ if (length == written) {
+ return;
+ }
+
+ // We need to finish the block to flush out the remaining bytes. We do so by inputting zeros,
+ // so that the corresponding bytes output by the cipher are those that would have been XORed
+ // against the real end-of-block data to transform it. We store these bytes so that we can
+ // perform the transformation manually in the case of a subsequent call to this method with
+ // the real data.
+ int bytesToFlush = length - written;
+ Assertions.checkState(bytesToFlush < blockSize);
+ outOffset += written;
+ pendingXorBytes = blockSize - bytesToFlush;
+ written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0);
+ Assertions.checkState(written == blockSize);
+ // The first part of xorBytes contains the flushed data, which we copy out. The remainder
+ // contains the bytes that will be needed for manual transformation in a subsequent call.
+ for (int i = 0; i < bytesToFlush; i++) {
+ out[outOffset++] = flushedBlock[i];
+ }
+ }
+
+ private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) {
+ try {
+ return cipher.update(in, inOffset, length, out, outOffset);
+ } catch (ShortBufferException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private byte[] getInitializationVector(long nonce, long counter) {
+ return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.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 com.google.android.exoplayer2.upstream.crypto;
+
+/**
+ * Utility functions for the crypto package.
+ */
+/* package */ final class CryptoUtil {
+
+ private CryptoUtil() {}
+
+ /**
+ * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash
+ * values produced by this function are less likely to collide than those produced by
+ * {@link #hashCode()}.
+ */
+ public static long getFNV64Hash(String input) {
+ if (input == null) {
+ return 0;
+ }
+
+ long hash = 0;
+ for (int i = 0; i < input.length(); i++) {
+ hash ^= input.charAt(i);
+ // This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number).
+ hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40);
+ }
+ return hash;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/Assertions.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 com.google.android.exoplayer2.util;
+
+import android.os.Looper;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+
+/**
+ * Provides methods for asserting the truth of expressions and properties.
+ */
+public final class Assertions {
+
+ private Assertions() {}
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @param errorMessage The exception message if an exception is thrown. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws {@link IndexOutOfBoundsException} if {@code index} falls outside the specified bounds.
+ *
+ * @param index The index to test.
+ * @param start The start of the allowed range (inclusive).
+ * @param limit The end of the allowed range (exclusive).
+ * @return The {@code index} that was validated.
+ * @throws IndexOutOfBoundsException If {@code index} falls outside the specified bounds.
+ */
+ public static int checkIndex(int index, int start, int limit) {
+ if (index < start || index >= limit) {
+ throw new IndexOutOfBoundsException();
+ }
+ return index;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @param errorMessage The exception message if an exception is thrown. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws {@link NullPointerException} if {@code reference} is null.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ public static <T> T checkNotNull(T reference) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link NullPointerException} if {@code reference} is null.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ public static <T> T checkNotNull(T reference, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.
+ *
+ * @param string The string to check.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ public static String checkNotEmpty(String string) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
+ throw new IllegalArgumentException();
+ }
+ return string;
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.
+ *
+ * @param string The string to check.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ public static String checkNotEmpty(String string, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ return string;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if the calling thread is not the application's main
+ * thread.
+ *
+ * @throws IllegalStateException If the calling thread is not the application's main thread.
+ */
+ public static void checkMainThread() {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("Not in applications main thread");
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/AtomicFile.java
@@ -0,0 +1,198 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A helper class for performing atomic operations on a file by creating a backup file until a write
+ * has successfully completed.
+ *
+ * <p>Atomic file guarantees file integrity by ensuring that a file has been completely written and
+ * sync'd to disk before removing its backup. As long as the backup file exists, the original file
+ * is considered to be invalid (left over from a previous attempt to write the file).
+ *
+ * <p>Atomic file does not confer any file locking semantics. Do not use this class when the file
+ * may be accessed or modified concurrently by multiple threads or processes. The caller is
+ * responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.
+ */
+public final class AtomicFile {
+
+ private static final String TAG = "AtomicFile";
+
+ private final File baseName;
+ private final File backupName;
+
+ /**
+ * Create a new AtomicFile for a file located at the given File path. The secondary backup file
+ * will be the same file path with ".bak" appended.
+ */
+ public AtomicFile(File baseName) {
+ this.baseName = baseName;
+ backupName = new File(baseName.getPath() + ".bak");
+ }
+
+ /** Delete the atomic file. This deletes both the base and backup files. */
+ public void delete() {
+ baseName.delete();
+ backupName.delete();
+ }
+
+ /**
+ * Start a new write operation on the file. This returns an {@link OutputStream} to which you can
+ * write the new file data. If the whole data is written successfully you <em>must</em> call
+ * {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()}
+ * only to free up resources used by it.
+ *
+ * <p>Example usage:
+ *
+ * <pre>
+ * DataOutputStream dataOutput = null;
+ * try {
+ * OutputStream outputStream = atomicFile.startWrite();
+ * dataOutput = new DataOutputStream(outputStream); // Wrapper stream
+ * dataOutput.write(data1);
+ * dataOutput.write(data2);
+ * atomicFile.endWrite(dataOutput); // Pass wrapper stream
+ * } finally{
+ * if (dataOutput != null) {
+ * dataOutput.close();
+ * }
+ * }
+ * </pre>
+ *
+ * <p>Note that if another thread is currently performing a write, this will simply replace
+ * whatever that thread is writing with the new file being written by this thread, and when the
+ * other thread finishes the write the new write operation will no longer be safe (or will be
+ * lost). You must do your own threading protection for access to AtomicFile.
+ */
+ public OutputStream startWrite() throws IOException {
+ // Rename the current file so it may be used as a backup during the next read
+ if (baseName.exists()) {
+ if (!backupName.exists()) {
+ if (!baseName.renameTo(backupName)) {
+ Log.w(TAG, "Couldn't rename file " + baseName + " to backup file " + backupName);
+ }
+ } else {
+ baseName.delete();
+ }
+ }
+ OutputStream str;
+ try {
+ str = new AtomicFileOutputStream(baseName);
+ } catch (FileNotFoundException e) {
+ File parent = baseName.getParentFile();
+ if (!parent.mkdirs()) {
+ throw new IOException("Couldn't create directory " + baseName);
+ }
+ try {
+ str = new AtomicFileOutputStream(baseName);
+ } catch (FileNotFoundException e2) {
+ throw new IOException("Couldn't create " + baseName);
+ }
+ }
+ return str;
+ }
+
+ /**
+ * Call when you have successfully finished writing to the stream returned by {@link
+ * #startWrite()}. This will close, sync, and commit the new data. The next attempt to read the
+ * atomic file will return the new file stream.
+ *
+ * @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link
+ * #startWrite()}.
+ * @see #startWrite()
+ */
+ public void endWrite(OutputStream str) throws IOException {
+ str.close();
+ // If close() throws exception, the next line is skipped.
+ backupName.delete();
+ }
+
+ /**
+ * Open the atomic file for reading. If there previously was an incomplete write, this will roll
+ * back to the last good data before opening for read.
+ *
+ * <p>Note that if another thread is currently performing a write, this will incorrectly consider
+ * it to be in the state of a bad write and roll back, causing the new data currently being
+ * written to be dropped. You must do your own threading protection for access to AtomicFile.
+ */
+ public InputStream openRead() throws FileNotFoundException {
+ restoreBackup();
+ return new FileInputStream(baseName);
+ }
+
+ private void restoreBackup() {
+ if (backupName.exists()) {
+ baseName.delete();
+ backupName.renameTo(baseName);
+ }
+ }
+
+ private static final class AtomicFileOutputStream extends OutputStream {
+
+ private final FileOutputStream fileOutputStream;
+ private boolean closed = false;
+
+ public AtomicFileOutputStream(File file) throws FileNotFoundException {
+ fileOutputStream = new FileOutputStream(file);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ flush();
+ try {
+ fileOutputStream.getFD().sync();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to sync file descriptor:", e);
+ }
+ fileOutputStream.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ fileOutputStream.flush();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ fileOutputStream.write(b);
+ }
+
+ @Override
+ public void write(@NonNull byte[] b) throws IOException {
+ fileOutputStream.write(b);
+ }
+
+ @Override
+ public void write(@NonNull byte[] b, int off, int len) throws IOException {
+ fileOutputStream.write(b, off, len);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/Clock.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+/**
+ * An interface through which system clocks can be read. The {@link SystemClock} implementation
+ * must be used for all non-test cases.
+ */
+public interface Clock {
+
+ /**
+ * Returns {@link android.os.SystemClock#elapsedRealtime}.
+ *
+ * @return Elapsed milliseconds since boot.
+ */
+ long elapsedRealtime();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
@@ -0,0 +1,272 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides static utility methods for manipulating various types of codec specific data.
+ */
+public final class CodecSpecificDataUtil {
+
+ private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ private static final int AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY = 0xF;
+
+ private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] {
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
+ };
+
+ private static final int AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID = -1;
+ /**
+ * In the channel configurations below, <A> indicates a single channel element; (A, B) indicates a
+ * channel pair element; and [A] indicates a low-frequency effects element.
+ * The speaker mapping short forms used are:
+ * - FC: front center
+ * - BC: back center
+ * - FL/FR: front left/right
+ * - FCL/FCR: front center left/right
+ * - FTL/FTR: front top left/right
+ * - SL/SR: back surround left/right
+ * - BL/BR: back left/right
+ * - LFE: low frequency effects
+ */
+ private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE =
+ new int[] {
+ 0,
+ 1, /* mono: <FC> */
+ 2, /* stereo: (FL, FR) */
+ 3, /* 3.0: <FC>, (FL, FR) */
+ 4, /* 4.0: <FC>, (FL, FR), <BC> */
+ 5, /* 5.0 back: <FC>, (FL, FR), (SL, SR) */
+ 6, /* 5.1 back: <FC>, (FL, FR), (SL, SR), <BC>, [LFE] */
+ 8, /* 7.1 wide back: <FC>, (FCL, FCR), (FL, FR), (SL, SR), [LFE] */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ 7, /* 6.1: <FC>, (FL, FR), (SL, SR), <RC>, [LFE] */
+ 8, /* 7.1: <FC>, (FL, FR), (SL, SR), (BL, BR), [LFE] */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ 8, /* 7.1 top: <FC>, (FL, FR), (SL, SR), [LFE], (FTL, FTR) */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID
+ };
+
+ // Advanced Audio Coding Low-Complexity profile.
+ private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2;
+ // Spectral Band Replication.
+ private static final int AUDIO_OBJECT_TYPE_SBR = 5;
+ // Error Resilient Bit-Sliced Arithmetic Coding.
+ private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22;
+ // Parametric Stereo.
+ private static final int AUDIO_OBJECT_TYPE_PS = 29;
+ // Escape code for extended audio object types.
+ private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31;
+
+ private CodecSpecificDataUtil() {}
+
+ /**
+ * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioSpecificConfig The AudioSpecificConfig to parse.
+ * @return A pair consisting of the sample rate in Hz and the channel count.
+ */
+ public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig) {
+ ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig);
+ int audioObjectType = getAacAudioObjectType(bitArray);
+ int sampleRate = getAacSamplingFrequency(bitArray);
+ int channelConfiguration = bitArray.readBits(4);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) {
+ // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with
+ // explicit signaling, we return the extension sampling frequency as the sample rate of the
+ // content; this is identical to the sample rate of the decoded output but may differ from
+ // the sample rate set above.
+ // Use the extensionSamplingFrequencyIndex.
+ sampleRate = getAacSamplingFrequency(bitArray);
+ audioObjectType = getAacAudioObjectType(bitArray);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) {
+ // Use the extensionChannelConfiguration.
+ channelConfiguration = bitArray.readBits(4);
+ }
+ }
+ int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration];
+ Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID);
+ return Pair.create(sampleRate, channelCount);
+ }
+
+ /**
+ * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param sampleRate The sample rate in Hz.
+ * @param numChannels The number of channels.
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChannels) {
+ int sampleRateIndex = C.INDEX_UNSET;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) {
+ if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) {
+ sampleRateIndex = i;
+ }
+ }
+ int channelConfig = C.INDEX_UNSET;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) {
+ if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {
+ channelConfig = i;
+ }
+ }
+ if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) {
+ throw new IllegalArgumentException("Invalid sample rate or number of channels: "
+ + sampleRate + ", " + numChannels);
+ }
+ return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig);
+ }
+
+ /**
+ * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioObjectType The audio object type.
+ * @param sampleRateIndex The sample rate index.
+ * @param channelConfig The channel configuration.
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sampleRateIndex,
+ int channelConfig) {
+ byte[] specificConfig = new byte[2];
+ specificConfig[0] = (byte) (((audioObjectType << 3) & 0xF8) | ((sampleRateIndex >> 1) & 0x07));
+ specificConfig[1] = (byte) (((sampleRateIndex << 7) & 0x80) | ((channelConfig << 3) & 0x78));
+ return specificConfig;
+ }
+
+ /**
+ * Constructs a NAL unit consisting of the NAL start code followed by the specified data.
+ *
+ * @param data An array containing the data that should follow the NAL start code.
+ * @param offset The start offset into {@code data}.
+ * @param length The number of bytes to copy from {@code data}
+ * @return The constructed NAL unit.
+ */
+ public static byte[] buildNalUnit(byte[] data, int offset, int length) {
+ byte[] nalUnit = new byte[length + NAL_START_CODE.length];
+ System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length);
+ System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length);
+ return nalUnit;
+ }
+
+ /**
+ * Splits an array of NAL units.
+ * <p>
+ * If the input consists of NAL start code delimited units, then the returned array consists of
+ * the split NAL units, each of which is still prefixed with the NAL start code. For any other
+ * input, null is returned.
+ *
+ * @param data An array of data.
+ * @return The individual NAL units, or null if the input did not consist of NAL start code
+ * delimited units.
+ */
+ public static byte[][] splitNalUnits(byte[] data) {
+ if (!isNalStartCode(data, 0)) {
+ // data does not consist of NAL start code delimited units.
+ return null;
+ }
+ List<Integer> starts = new ArrayList<>();
+ int nalUnitIndex = 0;
+ do {
+ starts.add(nalUnitIndex);
+ nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length);
+ } while (nalUnitIndex != C.INDEX_UNSET);
+ byte[][] split = new byte[starts.size()][];
+ for (int i = 0; i < starts.size(); i++) {
+ int startIndex = starts.get(i);
+ int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length;
+ byte[] nal = new byte[endIndex - startIndex];
+ System.arraycopy(data, startIndex, nal, 0, nal.length);
+ split[i] = nal;
+ }
+ return split;
+ }
+
+ /**
+ * Finds the next occurrence of the NAL start code from a given index.
+ *
+ * @param data The data in which to search.
+ * @param index The first index to test.
+ * @return The index of the first byte of the found start code, or {@link C#INDEX_UNSET}.
+ */
+ private static int findNalStartCode(byte[] data, int index) {
+ int endIndex = data.length - NAL_START_CODE.length;
+ for (int i = index; i <= endIndex; i++) {
+ if (isNalStartCode(data, i)) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Tests whether there exists a NAL start code at a given index.
+ *
+ * @param data The data.
+ * @param index The index to test.
+ * @return Whether there exists a start code that begins at {@code index}.
+ */
+ private static boolean isNalStartCode(byte[] data, int index) {
+ if (data.length - index <= NAL_START_CODE.length) {
+ return false;
+ }
+ for (int j = 0; j < NAL_START_CODE.length; j++) {
+ if (data[index + j] != NAL_START_CODE[j]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the AAC audio object type as specified in 14496-3 (2005) Table 1.14.
+ *
+ * @param bitArray The bit array containing the audio specific configuration.
+ * @return The audio object type.
+ */
+ private static int getAacAudioObjectType(ParsableBitArray bitArray) {
+ int audioObjectType = bitArray.readBits(5);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) {
+ audioObjectType = 32 + bitArray.readBits(6);
+ }
+ return audioObjectType;
+ }
+
+ /**
+ * Returns the AAC sampling frequency (or extension sampling frequency) as specified in 14496-3
+ * (2005) Table 1.13.
+ *
+ * @param bitArray The bit array containing the audio specific configuration.
+ * @return The sampling frequency.
+ */
+ private static int getAacSamplingFrequency(ParsableBitArray bitArray) {
+ int samplingFrequency;
+ int frequencyIndex = bitArray.readBits(4);
+ if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) {
+ samplingFrequency = bitArray.readBits(24);
+ } else {
+ Assertions.checkArgument(frequencyIndex < 13);
+ samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
+ }
+ return samplingFrequency;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/ColorParser.java
@@ -0,0 +1,274 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.text.TextUtils;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for color expressions found in styling formats, e.g. TTML and CSS.
+ *
+ * @see <a href="https://w3c.github.io/webvtt/#styling">WebVTT CSS Styling</a>
+ * @see <a href="https://www.w3.org/TR/ttml2/">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>
+ **/
+public final class ColorParser {
+
+ private static final String RGB = "rgb";
+ private static final String RGBA = "rgba";
+
+ private static final Pattern RGB_PATTERN = Pattern.compile(
+ "^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
+
+ private static final Pattern RGBA_PATTERN_INT_ALPHA = Pattern.compile(
+ "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
+
+ private static final Pattern RGBA_PATTERN_FLOAT_ALPHA = Pattern.compile(
+ "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d*\\.?\\d*?)\\)$");
+
+ private static final Map<String, Integer> COLOR_MAP;
+
+ /**
+ * Parses a TTML color expression.
+ *
+ * @param colorExpression The color expression.
+ * @return The parsed ARGB color.
+ */
+ public static int parseTtmlColor(String colorExpression) {
+ return parseColorInternal(colorExpression, false);
+ }
+
+ /**
+ * Parses a CSS color expression.
+ *
+ * @param colorExpression The color expression.
+ * @return The parsed ARGB color.
+ */
+ public static int parseCssColor(String colorExpression) {
+ return parseColorInternal(colorExpression, true);
+ }
+
+ private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) {
+ Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));
+ colorExpression = colorExpression.replace(" ", "");
+ if (colorExpression.charAt(0) == '#') {
+ // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF.
+ int color = (int) Long.parseLong(colorExpression.substring(1), 16);
+ if (colorExpression.length() == 7) {
+ // Set the alpha value
+ color |= 0xFF000000;
+ } else if (colorExpression.length() == 9) {
+ // We have #RRGGBBAA, but we need #AARRGGBB
+ color = ((color & 0xFF) << 24) | (color >>> 8);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ return color;
+ } else if (colorExpression.startsWith(RGBA)) {
+ Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA)
+ .matcher(colorExpression);
+ if (matcher.matches()) {
+ return argb(
+ alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4)))
+ : Integer.parseInt(matcher.group(4), 10),
+ Integer.parseInt(matcher.group(1), 10),
+ Integer.parseInt(matcher.group(2), 10),
+ Integer.parseInt(matcher.group(3), 10)
+ );
+ }
+ } else if (colorExpression.startsWith(RGB)) {
+ Matcher matcher = RGB_PATTERN.matcher(colorExpression);
+ if (matcher.matches()) {
+ return rgb(
+ Integer.parseInt(matcher.group(1), 10),
+ Integer.parseInt(matcher.group(2), 10),
+ Integer.parseInt(matcher.group(3), 10)
+ );
+ }
+ } else {
+ // we use our own color map
+ Integer color = COLOR_MAP.get(Util.toLowerInvariant(colorExpression));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+
+ private static int argb(int alpha, int red, int green, int blue) {
+ return (alpha << 24) | (red << 16) | (green << 8) | blue;
+ }
+
+ private static int rgb(int red, int green, int blue) {
+ return argb(0xFF, red, green, blue);
+ }
+
+ static {
+ COLOR_MAP = new HashMap<>();
+ COLOR_MAP.put("aliceblue", 0xFFF0F8FF);
+ COLOR_MAP.put("antiquewhite", 0xFFFAEBD7);
+ COLOR_MAP.put("aqua", 0xFF00FFFF);
+ COLOR_MAP.put("aquamarine", 0xFF7FFFD4);
+ COLOR_MAP.put("azure", 0xFFF0FFFF);
+ COLOR_MAP.put("beige", 0xFFF5F5DC);
+ COLOR_MAP.put("bisque", 0xFFFFE4C4);
+ COLOR_MAP.put("black", 0xFF000000);
+ COLOR_MAP.put("blanchedalmond", 0xFFFFEBCD);
+ COLOR_MAP.put("blue", 0xFF0000FF);
+ COLOR_MAP.put("blueviolet", 0xFF8A2BE2);
+ COLOR_MAP.put("brown", 0xFFA52A2A);
+ COLOR_MAP.put("burlywood", 0xFFDEB887);
+ COLOR_MAP.put("cadetblue", 0xFF5F9EA0);
+ COLOR_MAP.put("chartreuse", 0xFF7FFF00);
+ COLOR_MAP.put("chocolate", 0xFFD2691E);
+ COLOR_MAP.put("coral", 0xFFFF7F50);
+ COLOR_MAP.put("cornflowerblue", 0xFF6495ED);
+ COLOR_MAP.put("cornsilk", 0xFFFFF8DC);
+ COLOR_MAP.put("crimson", 0xFFDC143C);
+ COLOR_MAP.put("cyan", 0xFF00FFFF);
+ COLOR_MAP.put("darkblue", 0xFF00008B);
+ COLOR_MAP.put("darkcyan", 0xFF008B8B);
+ COLOR_MAP.put("darkgoldenrod", 0xFFB8860B);
+ COLOR_MAP.put("darkgray", 0xFFA9A9A9);
+ COLOR_MAP.put("darkgreen", 0xFF006400);
+ COLOR_MAP.put("darkgrey", 0xFFA9A9A9);
+ COLOR_MAP.put("darkkhaki", 0xFFBDB76B);
+ COLOR_MAP.put("darkmagenta", 0xFF8B008B);
+ COLOR_MAP.put("darkolivegreen", 0xFF556B2F);
+ COLOR_MAP.put("darkorange", 0xFFFF8C00);
+ COLOR_MAP.put("darkorchid", 0xFF9932CC);
+ COLOR_MAP.put("darkred", 0xFF8B0000);
+ COLOR_MAP.put("darksalmon", 0xFFE9967A);
+ COLOR_MAP.put("darkseagreen", 0xFF8FBC8F);
+ COLOR_MAP.put("darkslateblue", 0xFF483D8B);
+ COLOR_MAP.put("darkslategray", 0xFF2F4F4F);
+ COLOR_MAP.put("darkslategrey", 0xFF2F4F4F);
+ COLOR_MAP.put("darkturquoise", 0xFF00CED1);
+ COLOR_MAP.put("darkviolet", 0xFF9400D3);
+ COLOR_MAP.put("deeppink", 0xFFFF1493);
+ COLOR_MAP.put("deepskyblue", 0xFF00BFFF);
+ COLOR_MAP.put("dimgray", 0xFF696969);
+ COLOR_MAP.put("dimgrey", 0xFF696969);
+ COLOR_MAP.put("dodgerblue", 0xFF1E90FF);
+ COLOR_MAP.put("firebrick", 0xFFB22222);
+ COLOR_MAP.put("floralwhite", 0xFFFFFAF0);
+ COLOR_MAP.put("forestgreen", 0xFF228B22);
+ COLOR_MAP.put("fuchsia", 0xFFFF00FF);
+ COLOR_MAP.put("gainsboro", 0xFFDCDCDC);
+ COLOR_MAP.put("ghostwhite", 0xFFF8F8FF);
+ COLOR_MAP.put("gold", 0xFFFFD700);
+ COLOR_MAP.put("goldenrod", 0xFFDAA520);
+ COLOR_MAP.put("gray", 0xFF808080);
+ COLOR_MAP.put("green", 0xFF008000);
+ COLOR_MAP.put("greenyellow", 0xFFADFF2F);
+ COLOR_MAP.put("grey", 0xFF808080);
+ COLOR_MAP.put("honeydew", 0xFFF0FFF0);
+ COLOR_MAP.put("hotpink", 0xFFFF69B4);
+ COLOR_MAP.put("indianred", 0xFFCD5C5C);
+ COLOR_MAP.put("indigo", 0xFF4B0082);
+ COLOR_MAP.put("ivory", 0xFFFFFFF0);
+ COLOR_MAP.put("khaki", 0xFFF0E68C);
+ COLOR_MAP.put("lavender", 0xFFE6E6FA);
+ COLOR_MAP.put("lavenderblush", 0xFFFFF0F5);
+ COLOR_MAP.put("lawngreen", 0xFF7CFC00);
+ COLOR_MAP.put("lemonchiffon", 0xFFFFFACD);
+ COLOR_MAP.put("lightblue", 0xFFADD8E6);
+ COLOR_MAP.put("lightcoral", 0xFFF08080);
+ COLOR_MAP.put("lightcyan", 0xFFE0FFFF);
+ COLOR_MAP.put("lightgoldenrodyellow", 0xFFFAFAD2);
+ COLOR_MAP.put("lightgray", 0xFFD3D3D3);
+ COLOR_MAP.put("lightgreen", 0xFF90EE90);
+ COLOR_MAP.put("lightgrey", 0xFFD3D3D3);
+ COLOR_MAP.put("lightpink", 0xFFFFB6C1);
+ COLOR_MAP.put("lightsalmon", 0xFFFFA07A);
+ COLOR_MAP.put("lightseagreen", 0xFF20B2AA);
+ COLOR_MAP.put("lightskyblue", 0xFF87CEFA);
+ COLOR_MAP.put("lightslategray", 0xFF778899);
+ COLOR_MAP.put("lightslategrey", 0xFF778899);
+ COLOR_MAP.put("lightsteelblue", 0xFFB0C4DE);
+ COLOR_MAP.put("lightyellow", 0xFFFFFFE0);
+ COLOR_MAP.put("lime", 0xFF00FF00);
+ COLOR_MAP.put("limegreen", 0xFF32CD32);
+ COLOR_MAP.put("linen", 0xFFFAF0E6);
+ COLOR_MAP.put("magenta", 0xFFFF00FF);
+ COLOR_MAP.put("maroon", 0xFF800000);
+ COLOR_MAP.put("mediumaquamarine", 0xFF66CDAA);
+ COLOR_MAP.put("mediumblue", 0xFF0000CD);
+ COLOR_MAP.put("mediumorchid", 0xFFBA55D3);
+ COLOR_MAP.put("mediumpurple", 0xFF9370DB);
+ COLOR_MAP.put("mediumseagreen", 0xFF3CB371);
+ COLOR_MAP.put("mediumslateblue", 0xFF7B68EE);
+ COLOR_MAP.put("mediumspringgreen", 0xFF00FA9A);
+ COLOR_MAP.put("mediumturquoise", 0xFF48D1CC);
+ COLOR_MAP.put("mediumvioletred", 0xFFC71585);
+ COLOR_MAP.put("midnightblue", 0xFF191970);
+ COLOR_MAP.put("mintcream", 0xFFF5FFFA);
+ COLOR_MAP.put("mistyrose", 0xFFFFE4E1);
+ COLOR_MAP.put("moccasin", 0xFFFFE4B5);
+ COLOR_MAP.put("navajowhite", 0xFFFFDEAD);
+ COLOR_MAP.put("navy", 0xFF000080);
+ COLOR_MAP.put("oldlace", 0xFFFDF5E6);
+ COLOR_MAP.put("olive", 0xFF808000);
+ COLOR_MAP.put("olivedrab", 0xFF6B8E23);
+ COLOR_MAP.put("orange", 0xFFFFA500);
+ COLOR_MAP.put("orangered", 0xFFFF4500);
+ COLOR_MAP.put("orchid", 0xFFDA70D6);
+ COLOR_MAP.put("palegoldenrod", 0xFFEEE8AA);
+ COLOR_MAP.put("palegreen", 0xFF98FB98);
+ COLOR_MAP.put("paleturquoise", 0xFFAFEEEE);
+ COLOR_MAP.put("palevioletred", 0xFFDB7093);
+ COLOR_MAP.put("papayawhip", 0xFFFFEFD5);
+ COLOR_MAP.put("peachpuff", 0xFFFFDAB9);
+ COLOR_MAP.put("peru", 0xFFCD853F);
+ COLOR_MAP.put("pink", 0xFFFFC0CB);
+ COLOR_MAP.put("plum", 0xFFDDA0DD);
+ COLOR_MAP.put("powderblue", 0xFFB0E0E6);
+ COLOR_MAP.put("purple", 0xFF800080);
+ COLOR_MAP.put("rebeccapurple", 0xFF663399);
+ COLOR_MAP.put("red", 0xFFFF0000);
+ COLOR_MAP.put("rosybrown", 0xFFBC8F8F);
+ COLOR_MAP.put("royalblue", 0xFF4169E1);
+ COLOR_MAP.put("saddlebrown", 0xFF8B4513);
+ COLOR_MAP.put("salmon", 0xFFFA8072);
+ COLOR_MAP.put("sandybrown", 0xFFF4A460);
+ COLOR_MAP.put("seagreen", 0xFF2E8B57);
+ COLOR_MAP.put("seashell", 0xFFFFF5EE);
+ COLOR_MAP.put("sienna", 0xFFA0522D);
+ COLOR_MAP.put("silver", 0xFFC0C0C0);
+ COLOR_MAP.put("skyblue", 0xFF87CEEB);
+ COLOR_MAP.put("slateblue", 0xFF6A5ACD);
+ COLOR_MAP.put("slategray", 0xFF708090);
+ COLOR_MAP.put("slategrey", 0xFF708090);
+ COLOR_MAP.put("snow", 0xFFFFFAFA);
+ COLOR_MAP.put("springgreen", 0xFF00FF7F);
+ COLOR_MAP.put("steelblue", 0xFF4682B4);
+ COLOR_MAP.put("tan", 0xFFD2B48C);
+ COLOR_MAP.put("teal", 0xFF008080);
+ COLOR_MAP.put("thistle", 0xFFD8BFD8);
+ COLOR_MAP.put("tomato", 0xFFFF6347);
+ COLOR_MAP.put("transparent", 0x00000000);
+ COLOR_MAP.put("turquoise", 0xFF40E0D0);
+ COLOR_MAP.put("violet", 0xFFEE82EE);
+ COLOR_MAP.put("wheat", 0xFFF5DEB3);
+ COLOR_MAP.put("white", 0xFFFFFFFF);
+ COLOR_MAP.put("whitesmoke", 0xFFF5F5F5);
+ COLOR_MAP.put("yellow", 0xFFFFFF00);
+ COLOR_MAP.put("yellowgreen", 0xFF9ACD32);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/ConditionVariable.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+/**
+ * A condition variable whose {@link #open()} and {@link #close()} methods return whether they
+ * resulted in a change of state.
+ */
+public final class ConditionVariable {
+
+ private boolean isOpen;
+
+ /**
+ * Opens the condition and releases all threads that are blocked.
+ *
+ * @return True if the condition variable was opened. False if it was already open.
+ */
+ public synchronized boolean open() {
+ if (isOpen) {
+ return false;
+ }
+ isOpen = true;
+ notifyAll();
+ return true;
+ }
+
+ /**
+ * Closes the condition.
+ *
+ * @return True if the condition variable was closed. False if it was already closed.
+ */
+ public synchronized boolean close() {
+ boolean wasOpen = isOpen;
+ isOpen = false;
+ return wasOpen;
+ }
+
+ /**
+ * Blocks until the condition is opened.
+ *
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public synchronized void block() throws InterruptedException {
+ while (!isOpen) {
+ wait();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/FlacStreamInfo.java
@@ -0,0 +1,78 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+/**
+ * Holder for FLAC stream info.
+ */
+public final class FlacStreamInfo {
+
+ public final int minBlockSize;
+ public final int maxBlockSize;
+ public final int minFrameSize;
+ public final int maxFrameSize;
+ public final int sampleRate;
+ public final int channels;
+ public final int bitsPerSample;
+ public final long totalSamples;
+
+ /**
+ * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure.
+ *
+ * @param data An array holding FLAC stream info metadata structure
+ * @param offset Offset of the structure in the array
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
+ * METADATA_BLOCK_STREAMINFO</a>
+ */
+ public FlacStreamInfo(byte[] data, int offset) {
+ ParsableBitArray scratch = new ParsableBitArray(data);
+ scratch.setPosition(offset * 8);
+ this.minBlockSize = scratch.readBits(16);
+ this.maxBlockSize = scratch.readBits(16);
+ this.minFrameSize = scratch.readBits(24);
+ this.maxFrameSize = scratch.readBits(24);
+ this.sampleRate = scratch.readBits(20);
+ this.channels = scratch.readBits(3) + 1;
+ this.bitsPerSample = scratch.readBits(5) + 1;
+ this.totalSamples = scratch.readBits(36);
+ // Remaining 16 bytes is md5 value
+ }
+
+ public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize,
+ int sampleRate, int channels, int bitsPerSample, long totalSamples) {
+ this.minBlockSize = minBlockSize;
+ this.maxBlockSize = maxBlockSize;
+ this.minFrameSize = minFrameSize;
+ this.maxFrameSize = maxFrameSize;
+ this.sampleRate = sampleRate;
+ this.channels = channels;
+ this.bitsPerSample = bitsPerSample;
+ this.totalSamples = totalSamples;
+ }
+
+ public int maxDecodedFrameSize() {
+ return maxBlockSize * channels * 2;
+ }
+
+ public int bitRate() {
+ return bitsPerSample * sampleRate;
+ }
+
+ public long durationUs() {
+ return (totalSamples * 1000000L) / sampleRate;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/LibraryLoader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+/**
+ * Configurable loader for native libraries.
+ */
+public final class LibraryLoader {
+
+ private String[] nativeLibraries;
+ private boolean loadAttempted;
+ private boolean isAvailable;
+
+ /**
+ * @param libraries The names of the libraries to load.
+ */
+ public LibraryLoader(String... libraries) {
+ nativeLibraries = libraries;
+ }
+
+ /**
+ * Overrides the names of the libraries to load. Must be called before any call to
+ * {@link #isAvailable()}.
+ */
+ public synchronized void setLibraries(String... libraries) {
+ Assertions.checkState(!loadAttempted, "Cannot set libraries after loading");
+ nativeLibraries = libraries;
+ }
+
+ /**
+ * Returns whether the underlying libraries are available, loading them if necessary.
+ */
+ public synchronized boolean isAvailable() {
+ if (loadAttempted) {
+ return isAvailable;
+ }
+ loadAttempted = true;
+ try {
+ for (String lib : nativeLibraries) {
+ System.loadLibrary(lib);
+ }
+ isAvailable = true;
+ } catch (UnsatisfiedLinkError exception) {
+ // Do nothing.
+ }
+ return isAvailable;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/LongArray.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import java.util.Arrays;
+
+/**
+ * An append-only, auto-growing {@code long[]}.
+ */
+public final class LongArray {
+
+ private static final int DEFAULT_INITIAL_CAPACITY = 32;
+
+ private int size;
+ private long[] values;
+
+ public LongArray() {
+ this(DEFAULT_INITIAL_CAPACITY);
+ }
+
+ /**
+ * @param initialCapacity The initial capacity of the array.
+ */
+ public LongArray(int initialCapacity) {
+ values = new long[initialCapacity];
+ }
+
+ /**
+ * Appends a value.
+ *
+ * @param value The value to append.
+ */
+ public void add(long value) {
+ if (size == values.length) {
+ values = Arrays.copyOf(values, size * 2);
+ }
+ values[size++] = value;
+ }
+
+ /**
+ * Returns the value at a specified index.
+ *
+ * @param index The index.
+ * @return The corresponding value.
+ * @throws IndexOutOfBoundsException If the index is less than zero, or greater than or equal to
+ * {@link #size()}.
+ */
+ public long get(int index) {
+ if (index < 0 || index >= size) {
+ throw new IndexOutOfBoundsException("Invalid index " + index + ", size is " + size);
+ }
+ return values[index];
+ }
+
+ /**
+ * Returns the current size of the array.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Copies the current values into a newly allocated primitive array.
+ *
+ * @return The primitive array containing the copied values.
+ */
+ public long[] toArray() {
+ return Arrays.copyOf(values, size);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/MediaClock.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 com.google.android.exoplayer2.util;
+
+import com.google.android.exoplayer2.PlaybackParameters;
+
+/**
+ * Tracks the progression of media time.
+ */
+public interface MediaClock {
+
+ /**
+ * Returns the current media position in microseconds.
+ */
+ long getPositionUs();
+
+ /**
+ * Attempts to set the playback parameters and returns the active playback parameters, which may
+ * differ from those passed in.
+ *
+ * @param playbackParameters The playback parameters.
+ * @return The active playback parameters.
+ */
+ PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the active playback parameters.
+ */
+ PlaybackParameters getPlaybackParameters();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/MimeTypes.java
@@ -0,0 +1,265 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+
+/**
+ * Defines common MIME types and helper methods.
+ */
+public final class MimeTypes {
+
+ public static final String BASE_TYPE_VIDEO = "video";
+ public static final String BASE_TYPE_AUDIO = "audio";
+ public static final String BASE_TYPE_TEXT = "text";
+ public static final String BASE_TYPE_APPLICATION = "application";
+
+ public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
+ public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
+ public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp";
+ public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
+ public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc";
+ public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8";
+ public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
+ public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es";
+ public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2";
+ public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1";
+ public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
+
+ public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4";
+ public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";
+ public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm";
+ public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg";
+ public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1";
+ public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2";
+ public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw";
+ public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw";
+ public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw";
+ public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3";
+ public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3";
+ public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd";
+ public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts";
+ public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd";
+ public static final String AUDIO_DTS_EXPRESS = BASE_TYPE_AUDIO + "/vnd.dts.hd;profile=lbr";
+ public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis";
+ public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus";
+ public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
+ public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
+ public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac";
+ public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
+
+ public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
+
+ public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4";
+ public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
+ public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
+ public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
+ public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
+ public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708";
+ public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
+ public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
+ public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g";
+ public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt";
+ public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608";
+ public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc";
+ public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub";
+ public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs";
+ public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35";
+ public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion";
+ public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg";
+ public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
+
+ private MimeTypes() {}
+
+ /**
+ * Whether the top-level type of {@code mimeType} is audio.
+ *
+ * @param mimeType The mimeType to test.
+ * @return Whether the top level type is audio.
+ */
+ public static boolean isAudio(String mimeType) {
+ return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));
+ }
+
+ /**
+ * Whether the top-level type of {@code mimeType} is video.
+ *
+ * @param mimeType The mimeType to test.
+ * @return Whether the top level type is video.
+ */
+ public static boolean isVideo(String mimeType) {
+ return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));
+ }
+
+ /**
+ * Whether the top-level type of {@code mimeType} is text.
+ *
+ * @param mimeType The mimeType to test.
+ * @return Whether the top level type is text.
+ */
+ public static boolean isText(String mimeType) {
+ return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));
+ }
+
+ /**
+ * Whether the top-level type of {@code mimeType} is application.
+ *
+ * @param mimeType The mimeType to test.
+ * @return Whether the top level type is application.
+ */
+ public static boolean isApplication(String mimeType) {
+ return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));
+ }
+
+
+ /**
+ * Derives a video sample mimeType from a codecs attribute.
+ *
+ * @param codecs The codecs attribute.
+ * @return The derived video mimeType, or null if it could not be derived.
+ */
+ public static String getVideoMediaMimeType(String codecs) {
+ if (codecs == null) {
+ return null;
+ }
+ String[] codecList = codecs.split(",");
+ for (String codec : codecList) {
+ String mimeType = getMediaMimeType(codec);
+ if (mimeType != null && isVideo(mimeType)) {
+ return mimeType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Derives a audio sample mimeType from a codecs attribute.
+ *
+ * @param codecs The codecs attribute.
+ * @return The derived audio mimeType, or null if it could not be derived.
+ */
+ public static String getAudioMediaMimeType(String codecs) {
+ if (codecs == null) {
+ return null;
+ }
+ String[] codecList = codecs.split(",");
+ for (String codec : codecList) {
+ String mimeType = getMediaMimeType(codec);
+ if (mimeType != null && isAudio(mimeType)) {
+ return mimeType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Derives a mimeType from a codec identifier, as defined in RFC 6381.
+ *
+ * @param codec The codec identifier to derive.
+ * @return The mimeType, or null if it could not be derived.
+ */
+ public static String getMediaMimeType(String codec) {
+ if (codec == null) {
+ return null;
+ }
+ codec = codec.trim();
+ if (codec.startsWith("avc1") || codec.startsWith("avc3")) {
+ return MimeTypes.VIDEO_H264;
+ } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) {
+ return MimeTypes.VIDEO_H265;
+ } else if (codec.startsWith("vp9")) {
+ return MimeTypes.VIDEO_VP9;
+ } else if (codec.startsWith("vp8")) {
+ return MimeTypes.VIDEO_VP8;
+ } else if (codec.startsWith("mp4a")) {
+ return MimeTypes.AUDIO_AAC;
+ } else if (codec.startsWith("ac-3") || codec.startsWith("dac3")) {
+ return MimeTypes.AUDIO_AC3;
+ } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) {
+ return MimeTypes.AUDIO_E_AC3;
+ } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) {
+ return MimeTypes.AUDIO_DTS;
+ } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) {
+ return MimeTypes.AUDIO_DTS_HD;
+ } else if (codec.startsWith("opus")) {
+ return MimeTypes.AUDIO_OPUS;
+ } else if (codec.startsWith("vorbis")) {
+ return MimeTypes.AUDIO_VORBIS;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type.
+ * {@link C#TRACK_TYPE_UNKNOWN} if the mime type is not known or the mapping cannot be
+ * established.
+ *
+ * @param mimeType The mimeType.
+ * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type.
+ */
+ public static int getTrackType(String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return C.TRACK_TYPE_UNKNOWN;
+ } else if (isAudio(mimeType)) {
+ return C.TRACK_TYPE_AUDIO;
+ } else if (isVideo(mimeType)) {
+ return C.TRACK_TYPE_VIDEO;
+ } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType)
+ || APPLICATION_CEA708.equals(mimeType) || APPLICATION_MP4CEA608.equals(mimeType)
+ || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType)
+ || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType)
+ || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType)
+ || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) {
+ return C.TRACK_TYPE_TEXT;
+ } else if (APPLICATION_ID3.equals(mimeType)
+ || APPLICATION_EMSG.equals(mimeType)
+ || APPLICATION_SCTE35.equals(mimeType)
+ || APPLICATION_CAMERA_MOTION.equals(mimeType)) {
+ return C.TRACK_TYPE_METADATA;
+ } else {
+ return C.TRACK_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Equivalent to {@code getTrackType(getMediaMimeType(codec))}.
+ *
+ * @param codec The codec.
+ * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified codec.
+ */
+ public static int getTrackTypeOfCodec(String codec) {
+ return getTrackType(getMediaMimeType(codec));
+ }
+
+ /**
+ * Returns the top-level type of {@code mimeType}.
+ *
+ * @param mimeType The mimeType whose top-level type is required.
+ * @return The top-level type, or null if the mimeType is null.
+ */
+ private static String getTopLevelType(String mimeType) {
+ if (mimeType == null) {
+ return null;
+ }
+ int indexOfSlash = mimeType.indexOf('/');
+ if (indexOfSlash == -1) {
+ throw new IllegalArgumentException("Invalid mime type: " + mimeType);
+ }
+ return mimeType.substring(0, indexOfSlash);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/NalUnitUtil.java
@@ -0,0 +1,491 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.util.Log;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Utility methods for handling H.264/AVC and H.265/HEVC NAL units.
+ */
+public final class NalUnitUtil {
+
+ private static final String TAG = "NalUnitUtil";
+
+ /**
+ * Holds data parsed from a sequence parameter set NAL unit.
+ */
+ public static final class SpsData {
+
+ public final int seqParameterSetId;
+ public final int width;
+ public final int height;
+ public final float pixelWidthAspectRatio;
+ public final boolean separateColorPlaneFlag;
+ public final boolean frameMbsOnlyFlag;
+ public final int frameNumLength;
+ public final int picOrderCountType;
+ public final int picOrderCntLsbLength;
+ public final boolean deltaPicOrderAlwaysZeroFlag;
+
+ public SpsData(int seqParameterSetId, int width, int height, float pixelWidthAspectRatio,
+ boolean separateColorPlaneFlag, boolean frameMbsOnlyFlag, int frameNumLength,
+ int picOrderCountType, int picOrderCntLsbLength, boolean deltaPicOrderAlwaysZeroFlag) {
+ this.seqParameterSetId = seqParameterSetId;
+ this.width = width;
+ this.height = height;
+ this.pixelWidthAspectRatio = pixelWidthAspectRatio;
+ this.separateColorPlaneFlag = separateColorPlaneFlag;
+ this.frameMbsOnlyFlag = frameMbsOnlyFlag;
+ this.frameNumLength = frameNumLength;
+ this.picOrderCountType = picOrderCountType;
+ this.picOrderCntLsbLength = picOrderCntLsbLength;
+ this.deltaPicOrderAlwaysZeroFlag = deltaPicOrderAlwaysZeroFlag;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from a picture parameter set NAL unit.
+ */
+ public static final class PpsData {
+
+ public final int picParameterSetId;
+ public final int seqParameterSetId;
+ public final boolean bottomFieldPicOrderInFramePresentFlag;
+
+ public PpsData(int picParameterSetId, int seqParameterSetId,
+ boolean bottomFieldPicOrderInFramePresentFlag) {
+ this.picParameterSetId = picParameterSetId;
+ this.seqParameterSetId = seqParameterSetId;
+ this.bottomFieldPicOrderInFramePresentFlag = bottomFieldPicOrderInFramePresentFlag;
+ }
+
+ }
+
+ /** Four initial bytes that must prefix NAL units for decoding. */
+ public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ /** Value for aspect_ratio_idc indicating an extended aspect ratio, in H.264 and H.265 SPSs. */
+ public static final int EXTENDED_SAR = 0xFF;
+ /** Aspect ratios indexed by aspect_ratio_idc, in H.264 and H.265 SPSs. */
+ public static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {
+ 1f /* Unspecified. Assume square */,
+ 1f,
+ 12f / 11f,
+ 10f / 11f,
+ 16f / 11f,
+ 40f / 33f,
+ 24f / 11f,
+ 20f / 11f,
+ 32f / 11f,
+ 80f / 33f,
+ 18f / 11f,
+ 15f / 11f,
+ 64f / 33f,
+ 160f / 99f,
+ 4f / 3f,
+ 3f / 2f,
+ 2f
+ };
+
+ private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+ private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
+ private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39;
+
+ private static final Object scratchEscapePositionsLock = new Object();
+
+ /**
+ * Temporary store for positions of escape codes in {@link #unescapeStream(byte[], int)}. Guarded
+ * by {@link #scratchEscapePositionsLock}.
+ */
+ private static int[] scratchEscapePositions = new int[10];
+
+ /**
+ * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with
+ * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.
+ * <p>
+ * Executions of this method are mutually exclusive, so it should not be called with very large
+ * buffers.
+ *
+ * @param data The data to unescape.
+ * @param limit The limit (exclusive) of the data to unescape.
+ * @return The length of the unescaped data.
+ */
+ public static int unescapeStream(byte[] data, int limit) {
+ synchronized (scratchEscapePositionsLock) {
+ int position = 0;
+ int scratchEscapeCount = 0;
+ while (position < limit) {
+ position = findNextUnescapeIndex(data, position, limit);
+ if (position < limit) {
+ if (scratchEscapePositions.length <= scratchEscapeCount) {
+ // Grow scratchEscapePositions to hold a larger number of positions.
+ scratchEscapePositions = Arrays.copyOf(scratchEscapePositions,
+ scratchEscapePositions.length * 2);
+ }
+ scratchEscapePositions[scratchEscapeCount++] = position;
+ position += 3;
+ }
+ }
+
+ int unescapedLength = limit - scratchEscapeCount;
+ int escapedPosition = 0; // The position being read from.
+ int unescapedPosition = 0; // The position being written to.
+ for (int i = 0; i < scratchEscapeCount; i++) {
+ int nextEscapePosition = scratchEscapePositions[i];
+ int copyLength = nextEscapePosition - escapedPosition;
+ System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength);
+ unescapedPosition += copyLength;
+ data[unescapedPosition++] = 0;
+ data[unescapedPosition++] = 0;
+ escapedPosition += copyLength + 3;
+ }
+
+ int remainingLength = unescapedLength - unescapedPosition;
+ System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength);
+ return unescapedLength;
+ }
+ }
+
+ /**
+ * Discards data from the buffer up to the first SPS, where {@code data.position()} is interpreted
+ * as the length of the buffer.
+ * <p>
+ * When the method returns, {@code data.position()} will contain the new length of the buffer. If
+ * the buffer is not empty it is guaranteed to start with an SPS.
+ *
+ * @param data Buffer containing start code delimited NAL units.
+ */
+ public static void discardToSps(ByteBuffer data) {
+ int length = data.position();
+ int consecutiveZeros = 0;
+ int offset = 0;
+ while (offset + 1 < length) {
+ int value = data.get(offset) & 0xFF;
+ if (consecutiveZeros == 3) {
+ if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) {
+ // Copy from this NAL unit onwards to the start of the buffer.
+ ByteBuffer offsetData = data.duplicate();
+ offsetData.position(offset - 3);
+ offsetData.limit(length);
+ data.position(0);
+ data.put(offsetData);
+ return;
+ }
+ } else if (value == 0) {
+ consecutiveZeros++;
+ }
+ if (value != 0) {
+ consecutiveZeros = 0;
+ }
+ offset++;
+ }
+ // Empty the buffer if the SPS NAL unit was not found.
+ data.clear();
+ }
+
+ /**
+ * Returns whether the NAL unit with the specified header contains supplemental enhancement
+ * information.
+ *
+ * @param mimeType The sample MIME type.
+ * @param nalUnitHeaderFirstByte The first byte of nal_unit().
+ * @return Whether the NAL unit with the specified header is an SEI NAL unit.
+ */
+ public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) {
+ return (MimeTypes.VIDEO_H264.equals(mimeType)
+ && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI)
+ || (MimeTypes.VIDEO_H265.equals(mimeType)
+ && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI);
+ }
+
+ /**
+ * Returns the type of the NAL unit in {@code data} that starts at {@code offset}.
+ *
+ * @param data The data to search.
+ * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
+ * {@code data.length - 3} (exclusive).
+ * @return The type of the unit.
+ */
+ public static int getNalUnitType(byte[] data, int offset) {
+ return data[offset + 3] & 0x1F;
+ }
+
+ /**
+ * Returns the type of the H.265 NAL unit in {@code data} that starts at {@code offset}.
+ *
+ * @param data The data to search.
+ * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
+ * {@code data.length - 3} (exclusive).
+ * @return The type of the unit.
+ */
+ public static int getH265NalUnitType(byte[] data, int offset) {
+ return (data[offset + 3] & 0x7E) >> 1;
+ }
+
+ /**
+ * Parses an SPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
+ * 7.3.2.1.1.
+ *
+ * @param nalData A buffer containing escaped SPS data.
+ * @param nalOffset The offset of the NAL unit header in {@code nalData}.
+ * @param nalLimit The limit of the NAL unit in {@code nalData}.
+ * @return A parsed representation of the SPS data.
+ */
+ public static SpsData parseSpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {
+ ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);
+ data.skipBits(8); // nal_unit
+ int profileIdc = data.readBits(8);
+ data.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8)
+ int seqParameterSetId = data.readUnsignedExpGolombCodedInt();
+
+ int chromaFormatIdc = 1; // Default is 4:2:0
+ boolean separateColorPlaneFlag = false;
+ if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244
+ || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118
+ || profileIdc == 128 || profileIdc == 138) {
+ chromaFormatIdc = data.readUnsignedExpGolombCodedInt();
+ if (chromaFormatIdc == 3) {
+ separateColorPlaneFlag = data.readBit();
+ }
+ data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+ data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+ data.skipBits(1); // qpprime_y_zero_transform_bypass_flag
+ boolean seqScalingMatrixPresentFlag = data.readBit();
+ if (seqScalingMatrixPresentFlag) {
+ int limit = (chromaFormatIdc != 3) ? 8 : 12;
+ for (int i = 0; i < limit; i++) {
+ boolean seqScalingListPresentFlag = data.readBit();
+ if (seqScalingListPresentFlag) {
+ skipScalingList(data, i < 6 ? 16 : 64);
+ }
+ }
+ }
+ }
+
+ int frameNumLength = data.readUnsignedExpGolombCodedInt() + 4; // log2_max_frame_num_minus4 + 4
+ int picOrderCntType = data.readUnsignedExpGolombCodedInt();
+ int picOrderCntLsbLength = 0;
+ boolean deltaPicOrderAlwaysZeroFlag = false;
+ if (picOrderCntType == 0) {
+ // log2_max_pic_order_cnt_lsb_minus4 + 4
+ picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4;
+ } else if (picOrderCntType == 1) {
+ deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flag
+ data.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic
+ data.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field
+ long numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt();
+ for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
+ data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
+ }
+ }
+ data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
+ data.skipBits(1); // gaps_in_frame_num_value_allowed_flag
+
+ int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;
+ int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1;
+ boolean frameMbsOnlyFlag = data.readBit();
+ int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;
+ if (!frameMbsOnlyFlag) {
+ data.skipBits(1); // mb_adaptive_frame_field_flag
+ }
+
+ data.skipBits(1); // direct_8x8_inference_flag
+ int frameWidth = picWidthInMbs * 16;
+ int frameHeight = frameHeightInMbs * 16;
+ boolean frameCroppingFlag = data.readBit();
+ if (frameCroppingFlag) {
+ int frameCropLeftOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropRightOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropTopOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt();
+ int cropUnitX;
+ int cropUnitY;
+ if (chromaFormatIdc == 0) {
+ cropUnitX = 1;
+ cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0);
+ } else {
+ int subWidthC = (chromaFormatIdc == 3) ? 1 : 2;
+ int subHeightC = (chromaFormatIdc == 1) ? 2 : 1;
+ cropUnitX = subWidthC;
+ cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0));
+ }
+ frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;
+ frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
+ }
+
+ float pixelWidthHeightRatio = 1;
+ boolean vuiParametersPresentFlag = data.readBit();
+ if (vuiParametersPresentFlag) {
+ boolean aspectRatioInfoPresentFlag = data.readBit();
+ if (aspectRatioInfoPresentFlag) {
+ int aspectRatioIdc = data.readBits(8);
+ if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {
+ int sarWidth = data.readBits(16);
+ int sarHeight = data.readBits(16);
+ if (sarWidth != 0 && sarHeight != 0) {
+ pixelWidthHeightRatio = (float) sarWidth / sarHeight;
+ }
+ } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {
+ pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
+ } else {
+ Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
+ }
+ }
+ }
+
+ return new SpsData(seqParameterSetId, frameWidth, frameHeight, pixelWidthHeightRatio,
+ separateColorPlaneFlag, frameMbsOnlyFlag, frameNumLength, picOrderCntType,
+ picOrderCntLsbLength, deltaPicOrderAlwaysZeroFlag);
+ }
+
+ /**
+ * Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
+ * 7.3.2.2.
+ *
+ * @param nalData A buffer containing escaped PPS data.
+ * @param nalOffset The offset of the NAL unit header in {@code nalData}.
+ * @param nalLimit The limit of the NAL unit in {@code nalData}.
+ * @return A parsed representation of the PPS data.
+ */
+ public static PpsData parsePpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {
+ ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);
+ data.skipBits(8); // nal_unit
+ int picParameterSetId = data.readUnsignedExpGolombCodedInt();
+ int seqParameterSetId = data.readUnsignedExpGolombCodedInt();
+ data.skipBits(1); // entropy_coding_mode_flag
+ boolean bottomFieldPicOrderInFramePresentFlag = data.readBit();
+ return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag);
+ }
+
+ /**
+ * Finds the first NAL unit in {@code data}.
+ * <p>
+ * If {@code prefixFlags} is null then the first three bytes of a NAL unit must be entirely
+ * contained within the part of the array being searched in order for it to be found.
+ * <p>
+ * When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four
+ * bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same
+ * {@code prefixFlags} parameter to successive calls. State maintained in this parameter enables
+ * the detection of such NAL units. Note that when using this feature, the return value may be 3,
+ * 2 or 1 less than {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before
+ * the first byte in the current array.
+ *
+ * @param data The data to search.
+ * @param startOffset The offset (inclusive) in the data to start the search.
+ * @param endOffset The offset (exclusive) in the data to end the search.
+ * @param prefixFlags A boolean array whose first three elements are used to store the state
+ * required to detect NAL units where the NAL unit prefix spans array boundaries. The array
+ * must be at least 3 elements long.
+ * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
+ */
+ public static int findNalUnit(byte[] data, int startOffset, int endOffset,
+ boolean[] prefixFlags) {
+ int length = endOffset - startOffset;
+
+ Assertions.checkState(length >= 0);
+ if (length == 0) {
+ return endOffset;
+ }
+
+ if (prefixFlags != null) {
+ if (prefixFlags[0]) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 3;
+ } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 2;
+ } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0
+ && data[startOffset + 1] == 1) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 1;
+ }
+ }
+
+ int limit = endOffset - 1;
+ // We're looking for the NAL unit start code prefix 0x000001. The value of i tracks the index of
+ // the third byte.
+ for (int i = startOffset + 2; i < limit; i += 3) {
+ if ((data[i] & 0xFE) != 0) {
+ // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the
+ // loop advance the index by three.
+ } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) {
+ if (prefixFlags != null) {
+ clearPrefixFlags(prefixFlags);
+ }
+ return i - 2;
+ } else {
+ // There isn't a NAL prefix here, but there might be at the next position. We should
+ // only skip forward by one. The loop will skip forward by three, so subtract two here.
+ i -= 2;
+ }
+ }
+
+ if (prefixFlags != null) {
+ // True if the last three bytes in the data seen so far are {0,0,1}.
+ prefixFlags[0] = length > 2
+ ? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
+ : length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
+ : (prefixFlags[1] && data[endOffset - 1] == 1);
+ // True if the last two bytes in the data seen so far are {0,0}.
+ prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0
+ : prefixFlags[2] && data[endOffset - 1] == 0;
+ // True if the last byte in the data seen so far is {0}.
+ prefixFlags[2] = data[endOffset - 1] == 0;
+ }
+
+ return endOffset;
+ }
+
+ /**
+ * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}.
+ *
+ * @param prefixFlags The flags to clear.
+ */
+ public static void clearPrefixFlags(boolean[] prefixFlags) {
+ prefixFlags[0] = false;
+ prefixFlags[1] = false;
+ prefixFlags[2] = false;
+ }
+
+ private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) {
+ for (int i = offset; i < limit - 2; i++) {
+ if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) {
+ return i;
+ }
+ }
+ return limit;
+ }
+
+ private static void skipScalingList(ParsableNalUnitBitArray bitArray, int size) {
+ int lastScale = 8;
+ int nextScale = 8;
+ for (int i = 0; i < size; i++) {
+ if (nextScale != 0) {
+ int deltaScale = bitArray.readSignedExpGolombCodedInt();
+ nextScale = (lastScale + deltaScale + 256) % 256;
+ }
+ lastScale = (nextScale == 0) ? lastScale : nextScale;
+ }
+ }
+
+ private NalUnitUtil() {
+ // Prevent instantiation.
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/ParsableBitArray.java
@@ -0,0 +1,238 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a bitstream.
+ */
+public final class ParsableBitArray {
+
+ public byte[] data;
+
+ // The offset within the data, stored as the current byte offset, and the bit offset within that
+ // byte (from 0 to 7).
+ private int byteOffset;
+ private int bitOffset;
+ private int byteLimit;
+
+ /**
+ * Creates a new instance that initially has no backing data.
+ */
+ public ParsableBitArray() {}
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ */
+ public ParsableBitArray(byte[] data) {
+ this(data, data.length);
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ * @param limit The limit in bytes.
+ */
+ public ParsableBitArray(byte[] data, int limit) {
+ this.data = data;
+ byteLimit = limit;
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ */
+ public void reset(byte[] data) {
+ reset(data, data.length);
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ * @param limit The limit in bytes.
+ */
+ public void reset(byte[] data, int limit) {
+ this.data = data;
+ byteOffset = 0;
+ bitOffset = 0;
+ byteLimit = limit;
+ }
+
+ /**
+ * Returns the number of bits yet to be read.
+ */
+ public int bitsLeft() {
+ return (byteLimit - byteOffset) * 8 - bitOffset;
+ }
+
+ /**
+ * Returns the current bit offset.
+ */
+ public int getPosition() {
+ return byteOffset * 8 + bitOffset;
+ }
+
+ /**
+ * Returns the current byte offset. Must only be called when the position is byte aligned.
+ *
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public int getBytePosition() {
+ Assertions.checkState(bitOffset == 0);
+ return byteOffset;
+ }
+
+ /**
+ * Sets the current bit offset.
+ *
+ * @param position The position to set.
+ */
+ public void setPosition(int position) {
+ byteOffset = position / 8;
+ bitOffset = position - (byteOffset * 8);
+ assertValidOffset();
+ }
+
+ /**
+ * Skips bits and moves current reading position forward.
+ *
+ * @param n The number of bits to skip.
+ */
+ public void skipBits(int n) {
+ byteOffset += (n / 8);
+ bitOffset += (n % 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return Whether the bit is set.
+ */
+ public boolean readBit() {
+ return readBits(1) == 1;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom n bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ if (numBits == 0) {
+ return 0;
+ }
+
+ int returnValue = 0;
+
+ // Read as many whole bytes as we can.
+ int wholeBytes = (numBits / 8);
+ for (int i = 0; i < wholeBytes; i++) {
+ int byteValue;
+ if (bitOffset != 0) {
+ byteValue = ((data[byteOffset] & 0xFF) << bitOffset)
+ | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset));
+ } else {
+ byteValue = data[byteOffset];
+ }
+ numBits -= 8;
+ returnValue |= (byteValue & 0xFF) << numBits;
+ byteOffset++;
+ }
+
+ // Read any remaining bits.
+ if (numBits > 0) {
+ int nextBit = bitOffset + numBits;
+ byte writeMask = (byte) (0xFF >> (8 - numBits));
+
+ if (nextBit > 8) {
+ // Combine bits from current byte and next byte.
+ returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8)
+ | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask));
+ byteOffset++;
+ } else {
+ // Bits to be read only within current byte.
+ returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask);
+ if (nextBit == 8) {
+ byteOffset++;
+ }
+ }
+
+ bitOffset = nextBit % 8;
+ }
+
+ assertValidOffset();
+ return returnValue;
+ }
+
+ /**
+ * Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
+ */
+ public void byteAlign() {
+ if (bitOffset == 0) {
+ return;
+ }
+ bitOffset = 0;
+ byteOffset++;
+ assertValidOffset();
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position
+ * is byte aligned.
+ *
+ * @see System#arraycopy(Object, int, Object, int, int)
+ * @param buffer The array into which the read data should be written.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param length The number of bytes to read.
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public void readBytes(byte[] buffer, int offset, int length) {
+ Assertions.checkState(bitOffset == 0);
+ System.arraycopy(data, byteOffset, buffer, offset, length);
+ byteOffset += length;
+ assertValidOffset();
+ }
+
+ /**
+ * Skips the next {@code length} bytes. Must only be called when the position is byte aligned.
+ *
+ * @param length The number of bytes to read.
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public void skipBytes(int length) {
+ Assertions.checkState(bitOffset == 0);
+ byteOffset += length;
+ assertValidOffset();
+ }
+
+ private void assertValidOffset() {
+ // It is fine for position to be at the end of the array, but no further.
+ Assertions.checkState(byteOffset >= 0
+ && (bitOffset >= 0 && bitOffset < 8)
+ && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/ParsableByteArray.java
@@ -0,0 +1,565 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are
+ * parsed with the assumption that their constituent bytes are in big endian order.
+ */
+public final class ParsableByteArray {
+
+ public byte[] data;
+
+ private int position;
+ private int limit;
+
+ /**
+ * Creates a new instance that initially has no backing data.
+ */
+ public ParsableByteArray() {}
+
+ /**
+ * Creates a new instance with {@code limit} bytes and sets the limit.
+ *
+ * @param limit The limit to set.
+ */
+ public ParsableByteArray(int limit) {
+ this.data = new byte[limit];
+ this.limit = limit;
+ }
+
+ /**
+ * Creates a new instance wrapping {@code data}, and sets the limit to {@code data.length}.
+ *
+ * @param data The array to wrap.
+ */
+ public ParsableByteArray(byte[] data) {
+ this.data = data;
+ limit = data.length;
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ * @param limit The limit to set.
+ */
+ public ParsableByteArray(byte[] data, int limit) {
+ this.data = data;
+ this.limit = limit;
+ }
+
+ /**
+ * Resets the position to zero and the limit to the specified value. If the limit exceeds the
+ * capacity, {@code data} is replaced with a new array of sufficient size.
+ *
+ * @param limit The limit to set.
+ */
+ public void reset(int limit) {
+ reset(capacity() < limit ? new byte[limit] : data, limit);
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ * @param limit The limit to set.
+ */
+ public void reset(byte[] data, int limit) {
+ this.data = data;
+ this.limit = limit;
+ position = 0;
+ }
+
+ /**
+ * Sets the position and limit to zero.
+ */
+ public void reset() {
+ position = 0;
+ limit = 0;
+ }
+
+ /**
+ * Returns the number of bytes yet to be read.
+ */
+ public int bytesLeft() {
+ return limit - position;
+ }
+
+ /**
+ * Returns the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Sets the limit.
+ *
+ * @param limit The limit to set.
+ */
+ public void setLimit(int limit) {
+ Assertions.checkArgument(limit >= 0 && limit <= data.length);
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the current offset in the array, in bytes.
+ */
+ public int getPosition() {
+ return position;
+ }
+
+ /**
+ * Returns the capacity of the array, which may be larger than the limit.
+ */
+ public int capacity() {
+ return data == null ? 0 : data.length;
+ }
+
+ /**
+ * Sets the reading offset in the array.
+ *
+ * @param position Byte offset in the array from which to read.
+ * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
+ * array.
+ */
+ public void setPosition(int position) {
+ // It is fine for position to be at the end of the array.
+ Assertions.checkArgument(position >= 0 && position <= limit);
+ this.position = position;
+ }
+
+ /**
+ * Moves the reading offset by {@code bytes}.
+ *
+ * @param bytes The number of bytes to skip.
+ * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
+ * array.
+ */
+ public void skipBytes(int bytes) {
+ setPosition(position + bytes);
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of
+ * {@code bitArray} to zero.
+ *
+ * @param bitArray The {@link ParsableBitArray} into which the bytes should be read.
+ * @param length The number of bytes to write.
+ */
+ public void readBytes(ParsableBitArray bitArray, int length) {
+ readBytes(bitArray.data, 0, length);
+ bitArray.setPosition(0);
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer} at {@code offset}.
+ *
+ * @see System#arraycopy(Object, int, Object, int, int)
+ * @param buffer The array into which the read data should be written.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param length The number of bytes to read.
+ */
+ public void readBytes(byte[] buffer, int offset, int length) {
+ System.arraycopy(data, position, buffer, offset, length);
+ position += length;
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer}.
+ *
+ * @see ByteBuffer#put(byte[], int, int)
+ * @param buffer The {@link ByteBuffer} into which the read data should be written.
+ * @param length The number of bytes to read.
+ */
+ public void readBytes(ByteBuffer buffer, int length) {
+ buffer.put(data, position, length);
+ position += length;
+ }
+
+ /**
+ * Peeks at the next byte as an unsigned value.
+ */
+ public int peekUnsignedByte() {
+ return (data[position] & 0xFF);
+ }
+
+ /**
+ * Peeks at the next char.
+ */
+ public char peekChar() {
+ return (char) ((data[position] & 0xFF) << 8
+ | (data[position + 1] & 0xFF));
+ }
+
+ /**
+ * Reads the next byte as an unsigned value.
+ */
+ public int readUnsignedByte() {
+ return (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next two bytes as an unsigned value.
+ */
+ public int readUnsignedShort() {
+ return (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next two bytes as an unsigned value.
+ */
+ public int readLittleEndianUnsignedShort() {
+ return (data[position++] & 0xFF) | (data[position++] & 0xFF) << 8;
+ }
+
+ /**
+ * Reads the next two bytes as an signed value.
+ */
+ public short readShort() {
+ return (short) ((data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF));
+ }
+
+ /**
+ * Reads the next two bytes as a signed value.
+ */
+ public short readLittleEndianShort() {
+ return (short) ((data[position++] & 0xFF) | (data[position++] & 0xFF) << 8);
+ }
+
+ /**
+ * Reads the next three bytes as an unsigned value.
+ */
+ public int readUnsignedInt24() {
+ return (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next three bytes as a signed value in little endian order.
+ */
+ public int readLittleEndianInt24() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16;
+ }
+
+ /**
+ * Reads the next three bytes as an unsigned value in little endian order.
+ */
+ public int readLittleEndianUnsignedInt24() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16;
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned value.
+ */
+ public long readUnsignedInt() {
+ return (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL);
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned value in little endian order.
+ */
+ public long readLittleEndianUnsignedInt() {
+ return (data[position++] & 0xFFL)
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 24;
+ }
+
+ /**
+ * Reads the next four bytes as a signed value
+ */
+ public int readInt() {
+ return (data[position++] & 0xFF) << 24
+ | (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next four bytes as an signed value in little endian order.
+ */
+ public int readLittleEndianInt() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 24;
+ }
+
+ /**
+ * Reads the next eight bytes as a signed value.
+ */
+ public long readLong() {
+ return (data[position++] & 0xFFL) << 56
+ | (data[position++] & 0xFFL) << 48
+ | (data[position++] & 0xFFL) << 40
+ | (data[position++] & 0xFFL) << 32
+ | (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL);
+ }
+
+ /**
+ * Reads the next eight bytes as a signed value in little endian order.
+ */
+ public long readLittleEndianLong() {
+ return (data[position++] & 0xFFL)
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 32
+ | (data[position++] & 0xFFL) << 40
+ | (data[position++] & 0xFFL) << 48
+ | (data[position++] & 0xFFL) << 56;
+ }
+
+ /**
+ * Reads the next four bytes, returning the integer portion of the fixed point 16.16 integer.
+ */
+ public int readUnsignedFixedPoint1616() {
+ int result = (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ position += 2; // Skip the non-integer portion.
+ return result;
+ }
+
+ /**
+ * Reads a Synchsafe integer.
+ * <p>
+ * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can
+ * store 28 bits of information.
+ *
+ * @return The parsed value.
+ */
+ public int readSynchSafeInt() {
+ int b1 = readUnsignedByte();
+ int b2 = readUnsignedByte();
+ int b3 = readUnsignedByte();
+ int b4 = readUnsignedByte();
+ return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public int readUnsignedIntToInt() {
+ int result = readInt();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next four bytes as a little endian unsigned integer into an integer, if the top bit
+ * is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public int readLittleEndianUnsignedIntToInt() {
+ int result = readLittleEndianInt();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next eight bytes as an unsigned long into a long, if the top bit is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public long readUnsignedLongToLong() {
+ long result = readLong();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next four bytes as a 32-bit floating point value.
+ */
+ public float readFloat() {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ /**
+ * Reads the next eight bytes as a 64-bit floating point value.
+ */
+ public double readDouble() {
+ return Double.longBitsToDouble(readLong());
+ }
+
+ /**
+ * Reads the next {@code length} bytes as UTF-8 characters.
+ *
+ * @param length The number of bytes to read.
+ * @return The string encoded by the bytes.
+ */
+ public String readString(int length) {
+ return readString(length, Charset.defaultCharset());
+ }
+
+ /**
+ * Reads the next {@code length} bytes as characters in the specified {@link Charset}.
+ *
+ * @param length The number of bytes to read.
+ * @param charset The character set of the encoded characters.
+ * @return The string encoded by the bytes in the specified character set.
+ */
+ public String readString(int length, Charset charset) {
+ String result = new String(data, position, length, charset);
+ position += length;
+ return result;
+ }
+
+ /**
+ * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is discarded,
+ * if present.
+ *
+ * @param length The number of bytes to read.
+ * @return The string, not including any terminating NUL byte.
+ */
+ public String readNullTerminatedString(int length) {
+ if (length == 0) {
+ return "";
+ }
+ int stringLength = length;
+ int lastIndex = position + length - 1;
+ if (lastIndex < limit && data[lastIndex] == 0) {
+ stringLength--;
+ }
+ String result = new String(data, position, stringLength);
+ position += length;
+ return result;
+ }
+
+ /**
+ * Reads up to the next NUL byte (or the limit) as UTF-8 characters.
+ *
+ * @return The string not including any terminating NUL byte, or null if the end of the data has
+ * already been reached.
+ */
+ public String readNullTerminatedString() {
+ if (bytesLeft() == 0) {
+ return null;
+ }
+ int stringLimit = position;
+ while (stringLimit < limit && data[stringLimit] != 0) {
+ stringLimit++;
+ }
+ String string = new String(data, position, stringLimit - position);
+ position = stringLimit;
+ if (position < limit) {
+ position++;
+ }
+ return string;
+ }
+
+ /**
+ * Reads a line of text.
+ * <p>
+ * A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
+ * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default
+ * charset (UTF-8) is used.
+ *
+ * @return The line not including any line-termination characters, or null if the end of the data
+ * has already been reached.
+ */
+ public String readLine() {
+ if (bytesLeft() == 0) {
+ return null;
+ }
+ int lineLimit = position;
+ while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {
+ lineLimit++;
+ }
+ if (lineLimit - position >= 3 && data[position] == (byte) 0xEF
+ && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) {
+ // There's a byte order mark at the start of the line. Discard it.
+ position += 3;
+ }
+ String line = new String(data, position, lineLimit - position);
+ position = lineLimit;
+ if (position == limit) {
+ return line;
+ }
+ if (data[position] == '\r') {
+ position++;
+ if (position == limit) {
+ return line;
+ }
+ }
+ if (data[position] == '\n') {
+ position++;
+ }
+ return line;
+ }
+
+ /**
+ * Reads a long value encoded by UTF-8 encoding
+ *
+ * @throws NumberFormatException if there is a problem with decoding
+ * @return Decoded long value
+ */
+ public long readUtf8EncodedLong() {
+ int length = 0;
+ long value = data[position];
+ // find the high most 0 bit
+ for (int j = 7; j >= 0; j--) {
+ if ((value & (1 << j)) == 0) {
+ if (j < 6) {
+ value &= (1 << j) - 1;
+ length = 7 - j;
+ } else if (j == 7) {
+ length = 1;
+ }
+ break;
+ }
+ }
+ if (length == 0) {
+ throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value);
+ }
+ for (int i = 1; i < length; i++) {
+ int x = data[position + i];
+ if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th
+ throw new NumberFormatException("Invalid UTF-8 sequence continuation byte: " + value);
+ }
+ value = (value << 6) | (x & 0x3F);
+ }
+ position += length;
+ return value;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java
@@ -0,0 +1,227 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a NAL unit bitstream.
+ * <p>
+ * Whenever the byte sequence [0, 0, 3] appears in the wrapped byte array, it is treated as [0, 0]
+ * for all reading/skipping operations, which makes the bitstream appear to be unescaped.
+ */
+public final class ParsableNalUnitBitArray {
+
+ private byte[] data;
+ private int byteLimit;
+
+ // The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3].
+ private int byteOffset;
+ private int bitOffset;
+
+ /**
+ * @param data The data to wrap.
+ * @param offset The byte offset in {@code data} to start reading from.
+ * @param limit The byte offset of the end of the bitstream in {@code data}.
+ */
+ public ParsableNalUnitBitArray(byte[] data, int offset, int limit) {
+ reset(data, offset, limit);
+ }
+
+ /**
+ * Resets the wrapped data, limit and offset.
+ *
+ * @param data The data to wrap.
+ * @param offset The byte offset in {@code data} to start reading from.
+ * @param limit The byte offset of the end of the bitstream in {@code data}.
+ */
+ public void reset(byte[] data, int offset, int limit) {
+ this.data = data;
+ byteOffset = offset;
+ byteLimit = limit;
+ bitOffset = 0;
+ assertValidOffset();
+ }
+
+ /**
+ * Skips bits and moves current reading position forward.
+ *
+ * @param n The number of bits to skip.
+ */
+ public void skipBits(int n) {
+ int oldByteOffset = byteOffset;
+ byteOffset += (n / 8);
+ bitOffset += (n % 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ for (int i = oldByteOffset + 1; i <= byteOffset; i++) {
+ if (shouldSkipByte(i)) {
+ // Skip the byte and move forward to check three bytes ahead.
+ byteOffset++;
+ i += 2;
+ }
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Returns whether it's possible to read {@code n} bits starting from the current offset. The
+ * offset is not modified.
+ *
+ * @param n The number of bits.
+ * @return Whether it is possible to read {@code n} bits.
+ */
+ public boolean canReadBits(int n) {
+ int oldByteOffset = byteOffset;
+ int newByteOffset = byteOffset + (n / 8);
+ int newBitOffset = bitOffset + (n % 8);
+ if (newBitOffset > 7) {
+ newByteOffset++;
+ newBitOffset -= 8;
+ }
+ for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset < byteLimit; i++) {
+ if (shouldSkipByte(i)) {
+ // Skip the byte and move forward to check three bytes ahead.
+ newByteOffset++;
+ i += 2;
+ }
+ }
+ return newByteOffset < byteLimit || (newByteOffset == byteLimit && newBitOffset == 0);
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return Whether the bit is set.
+ */
+ public boolean readBit() {
+ return readBits(1) == 1;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom n bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ if (numBits == 0) {
+ return 0;
+ }
+
+ int returnValue = 0;
+
+ // Read as many whole bytes as we can.
+ int wholeBytes = (numBits / 8);
+ for (int i = 0; i < wholeBytes; i++) {
+ int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1;
+ int byteValue;
+ if (bitOffset != 0) {
+ byteValue = ((data[byteOffset] & 0xFF) << bitOffset)
+ | ((data[nextByteOffset] & 0xFF) >>> (8 - bitOffset));
+ } else {
+ byteValue = data[byteOffset];
+ }
+ numBits -= 8;
+ returnValue |= (byteValue & 0xFF) << numBits;
+ byteOffset = nextByteOffset;
+ }
+
+ // Read any remaining bits.
+ if (numBits > 0) {
+ int nextBit = bitOffset + numBits;
+ byte writeMask = (byte) (0xFF >> (8 - numBits));
+ int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1;
+
+ if (nextBit > 8) {
+ // Combine bits from current byte and next byte.
+ returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8)
+ | ((data[nextByteOffset] & 0xFF) >> (16 - nextBit))) & writeMask));
+ byteOffset = nextByteOffset;
+ } else {
+ // Bits to be read only within current byte.
+ returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask);
+ if (nextBit == 8) {
+ byteOffset = nextByteOffset;
+ }
+ }
+
+ bitOffset = nextBit % 8;
+ }
+
+ assertValidOffset();
+ return returnValue;
+ }
+
+ /**
+ * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current
+ * offset. The offset is not modified.
+ *
+ * @return Whether it is possible to read an Exp-Golomb-coded integer.
+ */
+ public boolean canReadExpGolombCodedNum() {
+ int initialByteOffset = byteOffset;
+ int initialBitOffset = bitOffset;
+ int leadingZeros = 0;
+ while (byteOffset < byteLimit && !readBit()) {
+ leadingZeros++;
+ }
+ boolean hitLimit = byteOffset == byteLimit;
+ byteOffset = initialByteOffset;
+ bitOffset = initialBitOffset;
+ return !hitLimit && canReadBits(leadingZeros * 2 + 1);
+ }
+
+ /**
+ * Reads an unsigned Exp-Golomb-coded format integer.
+ *
+ * @return The value of the parsed Exp-Golomb-coded integer.
+ */
+ public int readUnsignedExpGolombCodedInt() {
+ return readExpGolombCodeNum();
+ }
+
+ /**
+ * Reads an signed Exp-Golomb-coded format integer.
+ *
+ * @return The value of the parsed Exp-Golomb-coded integer.
+ */
+ public int readSignedExpGolombCodedInt() {
+ int codeNum = readExpGolombCodeNum();
+ return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
+ }
+
+ private int readExpGolombCodeNum() {
+ int leadingZeros = 0;
+ while (!readBit()) {
+ leadingZeros++;
+ }
+ return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
+ }
+
+ private boolean shouldSkipByte(int offset) {
+ return 2 <= offset && offset < byteLimit && data[offset] == (byte) 0x03
+ && data[offset - 2] == (byte) 0x00 && data[offset - 1] == (byte) 0x00;
+ }
+
+ private void assertValidOffset() {
+ // It is fine for position to be at the end of the array, but no further.
+ Assertions.checkState(byteOffset >= 0
+ && (bitOffset >= 0 && bitOffset < 8)
+ && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/Predicate.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+/**
+ * Determines a true of false value for a given input.
+ *
+ * @param <T> The input type of the predicate.
+ */
+public interface Predicate<T> {
+
+ /**
+ * Evaluates an input.
+ *
+ * @param input The input to evaluate.
+ * @return The evaluated result.
+ */
+ boolean evaluate(T input);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/PriorityTaskManager.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.PriorityQueue;
+
+/**
+ * Allows tasks with associated priorities to control how they proceed relative to one another.
+ * <p>
+ * A task should call {@link #add(int)} to register with the manager and {@link #remove(int)} to
+ * unregister. A registered task will prevent tasks of lower priority from proceeding, and should
+ * call {@link #proceed(int)}, {@link #proceedNonBlocking(int)} or {@link #proceedOrThrow(int)} each
+ * time it wishes to check whether it is itself allowed to proceed.
+ */
+public final class PriorityTaskManager {
+
+ /**
+ * Thrown when task attempts to proceed when another registered task has a higher priority.
+ */
+ public static class PriorityTooLowException extends IOException {
+
+ public PriorityTooLowException(int priority, int highestPriority) {
+ super("Priority too low [priority=" + priority + ", highest=" + highestPriority + "]");
+ }
+
+ }
+
+ private final Object lock = new Object();
+
+ // Guarded by lock.
+ private final PriorityQueue<Integer> queue;
+ private int highestPriority;
+
+ public PriorityTaskManager() {
+ queue = new PriorityQueue<>(10, Collections.reverseOrder());
+ highestPriority = Integer.MIN_VALUE;
+ }
+
+ /**
+ * Register a new task. The task must call {@link #remove(int)} when done.
+ *
+ * @param priority The priority of the task. Larger values indicate higher priorities.
+ */
+ public void add(int priority) {
+ synchronized (lock) {
+ queue.add(priority);
+ highestPriority = Math.max(highestPriority, priority);
+ }
+ }
+
+ /**
+ * Blocks until the task is allowed to proceed.
+ *
+ * @param priority The priority of the task.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public void proceed(int priority) throws InterruptedException {
+ synchronized (lock) {
+ while (highestPriority != priority) {
+ lock.wait();
+ }
+ }
+ }
+
+ /**
+ * A non-blocking variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task.
+ * @return Whether the task is allowed to proceed.
+ */
+ public boolean proceedNonBlocking(int priority) {
+ synchronized (lock) {
+ return highestPriority == priority;
+ }
+ }
+
+ /**
+ * A throwing variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task.
+ * @throws PriorityTooLowException If the task is not allowed to proceed.
+ */
+ public void proceedOrThrow(int priority) throws PriorityTooLowException {
+ synchronized (lock) {
+ if (highestPriority != priority) {
+ throw new PriorityTooLowException(priority, highestPriority);
+ }
+ }
+ }
+
+ /**
+ * Unregister a task.
+ *
+ * @param priority The priority of the task.
+ */
+ public void remove(int priority) {
+ synchronized (lock) {
+ queue.remove(priority);
+ highestPriority = queue.isEmpty() ? Integer.MIN_VALUE : queue.peek();
+ lock.notifyAll();
+ }
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This is a subclass of {@link BufferedOutputStream} with a {@link #reset(OutputStream)} method
+ * that allows an instance to be re-used with another underlying output stream.
+ */
+public final class ReusableBufferedOutputStream extends BufferedOutputStream {
+
+ private boolean closed;
+
+ public ReusableBufferedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public ReusableBufferedOutputStream(OutputStream out, int size) {
+ super(out, size);
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+
+ Throwable thrown = null;
+ try {
+ flush();
+ } catch (Throwable e) {
+ thrown = e;
+ }
+ try {
+ out.close();
+ } catch (Throwable e) {
+ if (thrown == null) {
+ thrown = e;
+ }
+ }
+ if (thrown != null) {
+ Util.sneakyThrow(thrown);
+ }
+ }
+
+ /**
+ * Resets this stream and uses the given output stream for writing. This stream must be closed
+ * before resetting.
+ *
+ * @param out New output stream to be used for writing.
+ * @throws IllegalStateException If the stream isn't closed.
+ */
+ public void reset(OutputStream out) {
+ Assertions.checkState(closed);
+ this.out = out;
+ count = 0;
+ closed = false;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/SlidingPercentile.java
@@ -0,0 +1,160 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Calculate any percentile over a sliding window of weighted values. A maximum weight is
+ * configured. Once the total weight of the values reaches the maximum weight, the oldest value is
+ * reduced in weight until it reaches zero and is removed. This maintains a constant total weight,
+ * equal to the maximum allowed, at the steady state.
+ * <p>
+ * This class can be used for bandwidth estimation based on a sliding window of past transfer rate
+ * observations. This is an alternative to sliding mean and exponential averaging which suffer from
+ * susceptibility to outliers and slow adaptation to step functions.
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/Moving_average">Wiki: Moving average</a>
+ * @see <a href="http://en.wikipedia.org/wiki/Selection_algorithm">Wiki: Selection algorithm</a>
+ */
+public class SlidingPercentile {
+
+ // Orderings.
+ private static final Comparator<Sample> INDEX_COMPARATOR = new Comparator<Sample>() {
+ @Override
+ public int compare(Sample a, Sample b) {
+ return a.index - b.index;
+ }
+ };
+
+ private static final Comparator<Sample> VALUE_COMPARATOR = new Comparator<Sample>() {
+ @Override
+ public int compare(Sample a, Sample b) {
+ return a.value < b.value ? -1 : b.value < a.value ? 1 : 0;
+ }
+ };
+
+ private static final int SORT_ORDER_NONE = -1;
+ private static final int SORT_ORDER_BY_VALUE = 0;
+ private static final int SORT_ORDER_BY_INDEX = 1;
+
+ private static final int MAX_RECYCLED_SAMPLES = 5;
+
+ private final int maxWeight;
+ private final ArrayList<Sample> samples;
+
+ private final Sample[] recycledSamples;
+
+ private int currentSortOrder;
+ private int nextSampleIndex;
+ private int totalWeight;
+ private int recycledSampleCount;
+
+ /**
+ * @param maxWeight The maximum weight.
+ */
+ public SlidingPercentile(int maxWeight) {
+ this.maxWeight = maxWeight;
+ recycledSamples = new Sample[MAX_RECYCLED_SAMPLES];
+ samples = new ArrayList<>();
+ currentSortOrder = SORT_ORDER_NONE;
+ }
+
+ /**
+ * Adds a new weighted value.
+ *
+ * @param weight The weight of the new observation.
+ * @param value The value of the new observation.
+ */
+ public void addSample(int weight, float value) {
+ ensureSortedByIndex();
+
+ Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
+ : new Sample();
+ newSample.index = nextSampleIndex++;
+ newSample.weight = weight;
+ newSample.value = value;
+ samples.add(newSample);
+ totalWeight += weight;
+
+ while (totalWeight > maxWeight) {
+ int excessWeight = totalWeight - maxWeight;
+ Sample oldestSample = samples.get(0);
+ if (oldestSample.weight <= excessWeight) {
+ totalWeight -= oldestSample.weight;
+ samples.remove(0);
+ if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {
+ recycledSamples[recycledSampleCount++] = oldestSample;
+ }
+ } else {
+ oldestSample.weight -= excessWeight;
+ totalWeight -= excessWeight;
+ }
+ }
+ }
+
+ /**
+ * Computes a percentile by integration.
+ *
+ * @param percentile The desired percentile, expressed as a fraction in the range (0,1].
+ * @return The requested percentile value or {@link Float#NaN} if no samples have been added.
+ */
+ public float getPercentile(float percentile) {
+ ensureSortedByValue();
+ float desiredWeight = percentile * totalWeight;
+ int accumulatedWeight = 0;
+ for (int i = 0; i < samples.size(); i++) {
+ Sample currentSample = samples.get(i);
+ accumulatedWeight += currentSample.weight;
+ if (accumulatedWeight >= desiredWeight) {
+ return currentSample.value;
+ }
+ }
+ // Clamp to maximum value or NaN if no values.
+ return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value;
+ }
+
+ /**
+ * Sorts the samples by index.
+ */
+ private void ensureSortedByIndex() {
+ if (currentSortOrder != SORT_ORDER_BY_INDEX) {
+ Collections.sort(samples, INDEX_COMPARATOR);
+ currentSortOrder = SORT_ORDER_BY_INDEX;
+ }
+ }
+
+ /**
+ * Sorts the samples by value.
+ */
+ private void ensureSortedByValue() {
+ if (currentSortOrder != SORT_ORDER_BY_VALUE) {
+ Collections.sort(samples, VALUE_COMPARATOR);
+ currentSortOrder = SORT_ORDER_BY_VALUE;
+ }
+ }
+
+ private static class Sample {
+
+ public int index;
+ public int weight;
+ public float value;
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java
@@ -0,0 +1,111 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
+
+/**
+ * A {@link MediaClock} whose position advances with real time based on the playback parameters when
+ * started.
+ */
+public final class StandaloneMediaClock implements MediaClock {
+
+ private boolean started;
+ private long baseUs;
+ private long baseElapsedMs;
+ private PlaybackParameters playbackParameters;
+
+ /**
+ * Creates a new standalone media clock.
+ */
+ public StandaloneMediaClock() {
+ playbackParameters = PlaybackParameters.DEFAULT;
+ }
+
+ /**
+ * Starts the clock. Does nothing if the clock is already started.
+ */
+ public void start() {
+ if (!started) {
+ baseElapsedMs = SystemClock.elapsedRealtime();
+ started = true;
+ }
+ }
+
+ /**
+ * Stops the clock. Does nothing if the clock is already stopped.
+ */
+ public void stop() {
+ if (started) {
+ setPositionUs(getPositionUs());
+ started = false;
+ }
+ }
+
+ /**
+ * Sets the clock's position.
+ *
+ * @param positionUs The position to set in microseconds.
+ */
+ public void setPositionUs(long positionUs) {
+ baseUs = positionUs;
+ if (started) {
+ baseElapsedMs = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Synchronizes this clock with the current state of {@code clock}.
+ *
+ * @param clock The clock with which to synchronize.
+ */
+ public void synchronize(MediaClock clock) {
+ setPositionUs(clock.getPositionUs());
+ playbackParameters = clock.getPlaybackParameters();
+ }
+
+ @Override
+ public long getPositionUs() {
+ long positionUs = baseUs;
+ if (started) {
+ long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs;
+ if (playbackParameters.speed == 1f) {
+ positionUs += C.msToUs(elapsedSinceBaseMs);
+ } else {
+ positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs);
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ // Store the current position as the new base, in case the playback speed has changed.
+ if (started) {
+ setPositionUs(getPositionUs());
+ }
+ this.playbackParameters = playbackParameters;
+ return playbackParameters;
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/SystemClock.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+/**
+ * The standard implementation of {@link Clock}.
+ */
+public final class SystemClock implements Clock {
+
+ @Override
+ public long elapsedRealtime() {
+ return android.os.SystemClock.elapsedRealtime();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/TimestampAdjuster.java
@@ -0,0 +1,188 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * Offsets timestamps according to an initial sample timestamp offset. MPEG-2 TS timestamps scaling
+ * and adjustment is supported, taking into account timestamp rollover.
+ */
+public final class TimestampAdjuster {
+
+ /**
+ * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should
+ * not be offset.
+ */
+ public static final long DO_NOT_OFFSET = Long.MAX_VALUE;
+
+ /**
+ * The value one greater than the largest representable (33 bit) MPEG-2 TS presentation timestamp.
+ */
+ private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
+
+ private long firstSampleTimestampUs;
+ private long timestampOffsetUs;
+
+ // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
+ private volatile long lastSampleTimestamp;
+
+ /**
+ * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.
+ */
+ public TimestampAdjuster(long firstSampleTimestampUs) {
+ lastSampleTimestamp = C.TIME_UNSET;
+ setFirstSampleTimestampUs(firstSampleTimestampUs);
+ }
+
+ /**
+ * Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be
+ * called before any timestamps have been adjusted.
+ *
+ * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or
+ * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
+ */
+ public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
+ Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET);
+ this.firstSampleTimestampUs = firstSampleTimestampUs;
+ }
+
+ /**
+ * Returns the first adjusted sample timestamp in microseconds.
+ *
+ * @return The first adjusted sample timestamp in microseconds.
+ */
+ public long getFirstSampleTimestampUs() {
+ return firstSampleTimestampUs;
+ }
+
+ /**
+ * Returns the last adjusted timestamp. If no timestamp has been adjusted, returns
+ * {@code firstSampleTimestampUs} as provided to the constructor. If this value is
+ * {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}.
+ *
+ * @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is
+ * returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is
+ * returned.
+ */
+ public long getLastAdjustedTimestampUs() {
+ return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp
+ : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
+ }
+
+ /**
+ * Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
+ * If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
+ * adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
+ *
+ * @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
+ * {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
+ * be offset.
+ */
+ public long getTimestampOffsetUs() {
+ return firstSampleTimestampUs == DO_NOT_OFFSET ? 0
+ : lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
+ }
+
+ /**
+ * Resets the instance to its initial state.
+ */
+ public void reset() {
+ lastSampleTimestamp = C.TIME_UNSET;
+ }
+
+ /**
+ * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.
+ *
+ * @param pts The MPEG-2 TS presentation timestamp.
+ * @return The adjusted timestamp in microseconds.
+ */
+ public long adjustTsTimestamp(long pts) {
+ if (pts == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ if (lastSampleTimestamp != C.TIME_UNSET) {
+ // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
+ // and we need to snap to the one closest to lastSampleTimestamp.
+ long lastPts = usToPts(lastSampleTimestamp);
+ long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
+ long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
+ long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount);
+ pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
+ ? ptsWrapBelow : ptsWrapAbove;
+ }
+ return adjustSampleTimestamp(ptsToUs(pts));
+ }
+
+ /**
+ * Offsets a sample timestamp in microseconds.
+ *
+ * @param timeUs The timestamp of a sample to adjust.
+ * @return The adjusted timestamp in microseconds.
+ */
+ public long adjustSampleTimestamp(long timeUs) {
+ if (timeUs == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ // Record the adjusted PTS to adjust for wraparound next time.
+ if (lastSampleTimestamp != C.TIME_UNSET) {
+ lastSampleTimestamp = timeUs;
+ } else {
+ if (firstSampleTimestampUs != DO_NOT_OFFSET) {
+ // Calculate the timestamp offset.
+ timestampOffsetUs = firstSampleTimestampUs - timeUs;
+ }
+ synchronized (this) {
+ lastSampleTimestamp = timeUs;
+ // Notify threads waiting for this adjuster to be initialized.
+ notifyAll();
+ }
+ }
+ return timeUs + timestampOffsetUs;
+ }
+
+ /**
+ * Blocks the calling thread until this adjuster is initialized.
+ *
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public synchronized void waitUntilInitialized() throws InterruptedException {
+ while (lastSampleTimestamp == C.TIME_UNSET) {
+ wait();
+ }
+ }
+
+ /**
+ * Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds.
+ *
+ * @param pts A value in MPEG-2 timestamp units.
+ * @return The corresponding value in microseconds.
+ */
+ public static long ptsToUs(long pts) {
+ return (pts * C.MICROS_PER_SECOND) / 90000;
+ }
+
+ /**
+ * Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units.
+ *
+ * @param us A value in microseconds.
+ * @return The corresponding value in MPEG-2 timestamp units.
+ */
+ public static long usToPts(long us) {
+ return (us * 90000) / C.MICROS_PER_SECOND;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/TraceUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.annotation.TargetApi;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+
+/**
+ * Calls through to {@link android.os.Trace} methods on supported API levels.
+ */
+public final class TraceUtil {
+
+ private TraceUtil() {}
+
+ /**
+ * Writes a trace message to indicate that a given section of code has begun.
+ *
+ * @see android.os.Trace#beginSection(String)
+ * @param sectionName The name of the code section to appear in the trace. This may be at most 127
+ * Unicode code units long.
+ */
+ public static void beginSection(String sectionName) {
+ if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {
+ beginSectionV18(sectionName);
+ }
+ }
+
+ /**
+ * Writes a trace message to indicate that a given section of code has ended.
+ *
+ * @see android.os.Trace#endSection()
+ */
+ public static void endSection() {
+ if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {
+ endSectionV18();
+ }
+ }
+
+ @TargetApi(18)
+ private static void beginSectionV18(String sectionName) {
+ android.os.Trace.beginSection(sectionName);
+ }
+
+ @TargetApi(18)
+ private static void endSectionV18() {
+ android.os.Trace.endSection();
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/UriUtil.java
@@ -0,0 +1,258 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Utility methods for manipulating URIs.
+ */
+public final class UriUtil {
+
+ /**
+ * The length of arrays returned by {@link #getUriIndices(String)}.
+ */
+ private static final int INDEX_COUNT = 4;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if
+ * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
+ * including when the URI has no scheme.
+ */
+ private static final int SCHEME_COLON = 0;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the path part. Equals (schemeColon + 1)
+ * if no authority part, (schemeColon + 3) if the authority part consists of just "//", and
+ * (query) if no path part. The characters starting at this index can be "//" only if the
+ * authority part is non-empty (in this case the double-slash means the first segment is empty).
+ */
+ private static final int PATH = 1;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the query part, including the '?'
+ * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a
+ * single '?' with no data.
+ */
+ private static final int QUERY = 2;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the fragment part, including the '#'
+ * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if
+ * the fragment part is a single '#' with no data.
+ */
+ private static final int FRAGMENT = 3;
+
+ private UriUtil() {}
+
+ /**
+ * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ public static Uri resolveToUri(String baseUri, String referenceUri) {
+ return Uri.parse(resolve(baseUri, referenceUri));
+ }
+
+ /**
+ * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}.
+ * <p>
+ * The resolution is performed as specified by RFC-3986.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ public static String resolve(String baseUri, String referenceUri) {
+ StringBuilder uri = new StringBuilder();
+
+ // Map null onto empty string, to make the following logic simpler.
+ baseUri = baseUri == null ? "" : baseUri;
+ referenceUri = referenceUri == null ? "" : referenceUri;
+
+ int[] refIndices = getUriIndices(referenceUri);
+ if (refIndices[SCHEME_COLON] != -1) {
+ // The reference is absolute. The target Uri is the reference.
+ uri.append(referenceUri);
+ removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]);
+ return uri.toString();
+ }
+
+ int[] baseIndices = getUriIndices(baseUri);
+ if (refIndices[FRAGMENT] == 0) {
+ // The reference is empty or contains just the fragment part, then the target Uri is the
+ // concatenation of the base Uri without its fragment, and the reference.
+ return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString();
+ }
+
+ if (refIndices[QUERY] == 0) {
+ // The reference starts with the query part. The target is the base up to (but excluding) the
+ // query, plus the reference.
+ return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString();
+ }
+
+ if (refIndices[PATH] != 0) {
+ // The reference has authority. The target is the base scheme plus the reference.
+ int baseLimit = baseIndices[SCHEME_COLON] + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+
+ if (referenceUri.charAt(refIndices[PATH]) == '/') {
+ // The reference path is rooted. The target is the base scheme and authority (if any), plus
+ // the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]);
+ }
+
+ // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
+ // and the reference. This can be split into 2 cases:
+ if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]
+ && baseIndices[PATH] == baseIndices[QUERY]) {
+ // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is
+ // needed after the authority, before appending the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1);
+ } else {
+ // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after
+ // it. If base hier-part has no '/', it could only mean that it is completely empty or
+ // contains only one segment, in which case the whole hier-part is excluded and the reference
+ // is appended right after the base scheme colon without an added '/'.
+ int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1);
+ int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+ }
+
+ /**
+ * Removes dot segments from the path of a URI.
+ *
+ * @param uri A {@link StringBuilder} containing the URI.
+ * @param offset The index of the start of the path in {@code uri}.
+ * @param limit The limit (exclusive) of the path in {@code uri}.
+ */
+ private static String removeDotSegments(StringBuilder uri, int offset, int limit) {
+ if (offset >= limit) {
+ // Nothing to do.
+ return uri.toString();
+ }
+ if (uri.charAt(offset) == '/') {
+ // If the path starts with a /, always retain it.
+ offset++;
+ }
+ // The first character of the current path segment.
+ int segmentStart = offset;
+ int i = offset;
+ while (i <= limit) {
+ int nextSegmentStart;
+ if (i == limit) {
+ nextSegmentStart = i;
+ } else if (uri.charAt(i) == '/') {
+ nextSegmentStart = i + 1;
+ } else {
+ i++;
+ continue;
+ }
+ // We've encountered the end of a segment or the end of the path. If the final segment was
+ // "." or "..", remove the appropriate segments of the path.
+ if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') {
+ // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
+ uri.delete(segmentStart, nextSegmentStart);
+ limit -= nextSegmentStart - segmentStart;
+ i = segmentStart;
+ } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.'
+ && uri.charAt(segmentStart + 1) == '.') {
+ // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
+ int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1;
+ int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset;
+ uri.delete(removeFrom, nextSegmentStart);
+ limit -= nextSegmentStart - removeFrom;
+ segmentStart = prevSegmentStart;
+ i = prevSegmentStart;
+ } else {
+ i++;
+ segmentStart = i;
+ }
+ }
+ return uri.toString();
+ }
+
+ /**
+ * Calculates indices of the constituent components of a URI.
+ *
+ * @param uriString The URI as a string.
+ * @return The corresponding indices.
+ */
+ private static int[] getUriIndices(String uriString) {
+ int[] indices = new int[INDEX_COUNT];
+ if (TextUtils.isEmpty(uriString)) {
+ indices[SCHEME_COLON] = -1;
+ return indices;
+ }
+
+ // Determine outer structure from right to left.
+ // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+ int length = uriString.length();
+ int fragmentIndex = uriString.indexOf('#');
+ if (fragmentIndex == -1) {
+ fragmentIndex = length;
+ }
+ int queryIndex = uriString.indexOf('?');
+ if (queryIndex == -1 || queryIndex > fragmentIndex) {
+ // '#' before '?': '?' is within the fragment.
+ queryIndex = fragmentIndex;
+ }
+ // Slashes are allowed only in hier-part so any colon after the first slash is part of the
+ // hier-part, not the scheme colon separator.
+ int schemeIndexLimit = uriString.indexOf('/');
+ if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
+ schemeIndexLimit = queryIndex;
+ }
+ int schemeIndex = uriString.indexOf(':');
+ if (schemeIndex > schemeIndexLimit) {
+ // '/' before ':'
+ schemeIndex = -1;
+ }
+
+ // Determine hier-part structure: hier-part = "//" authority path / path
+ // This block can also cope with schemeIndex == -1.
+ boolean hasAuthority = schemeIndex + 2 < queryIndex
+ && uriString.charAt(schemeIndex + 1) == '/'
+ && uriString.charAt(schemeIndex + 2) == '/';
+ int pathIndex;
+ if (hasAuthority) {
+ pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://"
+ if (pathIndex == -1 || pathIndex > queryIndex) {
+ pathIndex = queryIndex;
+ }
+ } else {
+ pathIndex = schemeIndex + 1;
+ }
+
+ indices[SCHEME_COLON] = schemeIndex;
+ indices[PATH] = pathIndex;
+ indices[QUERY] = queryIndex;
+ indices[FRAGMENT] = fragmentIndex;
+ return indices;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/Util.java
@@ -0,0 +1,1179 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+import android.Manifest.permission;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Formatter;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Miscellaneous utility methods.
+ */
+public final class Util {
+
+ /**
+ * Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently
+ * overridden for local testing.
+ */
+ public static final int SDK_INT =
+ (Build.VERSION.SDK_INT == 25 && Build.VERSION.CODENAME.charAt(0) == 'O') ? 26
+ : Build.VERSION.SDK_INT;
+
+ /**
+ * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local
+ * testing.
+ */
+ public static final String DEVICE = Build.DEVICE;
+
+ /**
+ * Like {@link Build#MANUFACTURER}, but in a place where it can be conveniently overridden for
+ * local testing.
+ */
+ public static final String MANUFACTURER = Build.MANUFACTURER;
+
+ /**
+ * Like {@link Build#MODEL}, but in a place where it can be conveniently overridden for local
+ * testing.
+ */
+ public static final String MODEL = Build.MODEL;
+
+ /**
+ * A concise description of the device that it can be useful to log for debugging purposes.
+ */
+ public static final String DEVICE_DEBUG_INFO = DEVICE + ", " + MODEL + ", " + MANUFACTURER + ", "
+ + SDK_INT;
+
+ private static final String TAG = "Util";
+ private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(
+ "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
+ + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?");
+ private static final Pattern XS_DURATION_PATTERN =
+ Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
+ private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
+
+ private Util() {}
+
+ /**
+ * Converts the entirety of an {@link InputStream} to a byte array.
+ *
+ * @param inputStream the {@link InputStream} to be read. The input stream is not closed by this
+ * method.
+ * @return a byte array containing all of the inputStream's bytes.
+ * @throws IOException if an error occurs reading from the stream.
+ */
+ public static byte[] toByteArray(InputStream inputStream) throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ return outputStream.toByteArray();
+ }
+
+ /**
+ * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE}
+ * permission read the specified {@link Uri}s, requesting the permission if necessary.
+ *
+ * @param activity The host activity for checking and requesting the permission.
+ * @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read.
+ * @return Whether a permission request was made.
+ */
+ @TargetApi(23)
+ public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
+ if (Util.SDK_INT < 23) {
+ return false;
+ }
+ for (Uri uri : uris) {
+ if (Util.isLocalFileUri(uri)) {
+ if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0);
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the URI is a path to a local file or a reference to a local file.
+ *
+ * @param uri The uri to test.
+ */
+ public static boolean isLocalFileUri(Uri uri) {
+ String scheme = uri.getScheme();
+ return TextUtils.isEmpty(scheme) || scheme.equals("file");
+ }
+
+ /**
+ * Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
+ * both may be null.
+ *
+ * @param o1 The first object.
+ * @param o2 The second object.
+ * @return {@code o1 == null ? o2 == null : o1.equals(o2)}.
+ */
+ public static boolean areEqual(Object o1, Object o2) {
+ return o1 == null ? o2 == null : o1.equals(o2);
+ }
+
+ /**
+ * Tests whether an {@code items} array contains an object equal to {@code item}, according to
+ * {@link Object#equals(Object)}.
+ * <p>
+ * If {@code item} is null then true is returned if and only if {@code items} contains null.
+ *
+ * @param items The array of items to search.
+ * @param item The item to search for.
+ * @return True if the array contains an object equal to the item being searched for.
+ */
+ public static boolean contains(Object[] items, Object item) {
+ for (Object arrayItem : items) {
+ if (Util.areEqual(arrayItem, item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Instantiates a new single threaded executor whose thread has the specified name.
+ *
+ * @param threadName The name of the thread.
+ * @return The executor.
+ */
+ public static ExecutorService newSingleThreadExecutor(final String threadName) {
+ return Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(@NonNull Runnable r) {
+ return new Thread(r, threadName);
+ }
+ });
+ }
+
+ /**
+ * Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
+ *
+ * @param dataSource The {@link DataSource} to close.
+ */
+ public static void closeQuietly(DataSource dataSource) {
+ try {
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link
+ * java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
+ *
+ * @param closeable The {@link Closeable} to close.
+ */
+ public static void closeQuietly(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Returns a normalized RFC 5646 language code.
+ *
+ * @param language A possibly non-normalized RFC 5646 language code.
+ * @return The normalized code, or null if the input was null.
+ */
+ public static String normalizeLanguageCode(String language) {
+ return language == null ? null : new Locale(language).getLanguage();
+ }
+
+ /**
+ * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
+ *
+ * @param value The {@link String} whose bytes should be obtained.
+ * @return The code points encoding using UTF-8.
+ */
+ public static byte[] getUtf8Bytes(String value) {
+ return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android.
+ }
+
+ /**
+ * Returns whether the given character is a carriage return ('\r') or a line feed ('\n').
+ *
+ * @param c The character.
+ * @return Whether the given character is a linebreak.
+ */
+ public static boolean isLinebreak(int c) {
+ return c == '\n' || c == '\r';
+ }
+
+ /**
+ * Converts text to lower case using {@link Locale#US}.
+ *
+ * @param text The text to convert.
+ * @return The lower case text, or null if {@code text} is null.
+ */
+ public static String toLowerInvariant(String text) {
+ return text == null ? null : text.toLowerCase(Locale.US);
+ }
+
+ /**
+ * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
+ *
+ * @param numerator The numerator to divide.
+ * @param denominator The denominator to divide by.
+ * @return The ceiled result of the division.
+ */
+ public static int ceilDivide(int numerator, int denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ /**
+ * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
+ *
+ * @param numerator The numerator to divide.
+ * @param denominator The denominator to divide by.
+ * @return The ceiled result of the division.
+ */
+ public static long ceilDivide(long numerator, long denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static int constrainValue(int value, int min, int max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static long constrainValue(long value, long min, long max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static float constrainValue(float value, float min, float max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Returns the index of the largest element in {@code array} that is less than (or optionally
+ * equal to) a specified {@code value}.
+ * <p>
+ * The search is performed using a binary search algorithm, so the array must be sorted. If the
+ * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the first one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the largest element strictly less
+ * than the value.
+ * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
+ * the smallest element in the array. If false then -1 will be returned.
+ * @return The index of the largest element in {@code array} that is less than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchFloor(int[] array, int value, boolean inclusive,
+ boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ while ((--index) >= 0 && array[index] == value) {}
+ if (inclusive) {
+ index++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the largest element in {@code array} that is less than (or optionally
+ * equal to) a specified {@code value}.
+ * <p>
+ * The search is performed using a binary search algorithm, so the array must be sorted. If the
+ * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the first one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the largest element strictly less
+ * than the value.
+ * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
+ * the smallest element in the array. If false then -1 will be returned.
+ * @return The index of the largest element in {@code array} that is less than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchFloor(long[] array, long value, boolean inclusive,
+ boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ while ((--index) >= 0 && array[index] == value) {}
+ if (inclusive) {
+ index++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) a specified {@code value}.
+ * <p>
+ * The search is performed using a binary search algorithm, so the array must be sorted. If
+ * the array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the last one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the smallest element strictly
+ * greater than the value.
+ * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
+ * value is greater than the largest element in the array. If false then {@code a.length} will
+ * be returned.
+ * @return The index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchCeil(long[] array, long value, boolean inclusive,
+ boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = ~index;
+ } else {
+ while ((++index) < array.length && array[index] == value) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ return stayInBounds ? Math.min(array.length - 1, index) : index;
+ }
+
+ /**
+ * Returns the index of the largest element in {@code list} that is less than (or optionally equal
+ * to) a specified {@code value}.
+ * <p>
+ * The search is performed using a binary search algorithm, so the list must be sorted. If the
+ * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the first one will be returned.
+ *
+ * @param <T> The type of values being searched.
+ * @param list The list to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the list, whether to return the corresponding
+ * index. If false then the returned index corresponds to the largest element strictly less
+ * than the value.
+ * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
+ * the smallest element in the list. If false then -1 will be returned.
+ * @return The index of the largest element in {@code list} that is less than (or optionally equal
+ * to) {@code value}.
+ */
+ public static <T> int binarySearchFloor(List<? extends Comparable<? super T>> list, T value,
+ boolean inclusive, boolean stayInBounds) {
+ int index = Collections.binarySearch(list, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ while ((--index) >= 0 && list.get(index).compareTo(value) == 0) {}
+ if (inclusive) {
+ index++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code list} that is greater than (or optionally
+ * equal to) a specified value.
+ * <p>
+ * The search is performed using a binary search algorithm, so the list must be sorted. If the
+ * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the last one will be returned.
+ *
+ * @param <T> The type of values being searched.
+ * @param list The list to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the list, whether to return the corresponding
+ * index. If false then the returned index corresponds to the smallest element strictly
+ * greater than the value.
+ * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
+ * the value is greater than the largest element in the list. If false then
+ * {@code list.size()} will be returned.
+ * @return The index of the smallest element in {@code list} that is greater than (or optionally
+ * equal to) {@code value}.
+ */
+ public static <T> int binarySearchCeil(List<? extends Comparable<? super T>> list, T value,
+ boolean inclusive, boolean stayInBounds) {
+ int index = Collections.binarySearch(list, value);
+ if (index < 0) {
+ index = ~index;
+ } else {
+ int listSize = list.size();
+ while ((++index) < listSize && list.get(index).compareTo(value) == 0) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ return stayInBounds ? Math.min(list.size() - 1, index) : index;
+ }
+
+ /**
+ * Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
+ *
+ * @param value The attribute value to decode.
+ * @return The parsed duration in milliseconds.
+ */
+ public static long parseXsDuration(String value) {
+ Matcher matcher = XS_DURATION_PATTERN.matcher(value);
+ if (matcher.matches()) {
+ boolean negated = !TextUtils.isEmpty(matcher.group(1));
+ // Durations containing years and months aren't completely defined. We assume there are
+ // 30.4368 days in a month, and 365.242 days in a year.
+ String years = matcher.group(3);
+ double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0;
+ String months = matcher.group(5);
+ durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0;
+ String days = matcher.group(7);
+ durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0;
+ String hours = matcher.group(10);
+ durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
+ String minutes = matcher.group(12);
+ durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
+ String seconds = matcher.group(14);
+ durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
+ long durationMillis = (long) (durationSeconds * 1000);
+ return negated ? -durationMillis : durationMillis;
+ } else {
+ return (long) (Double.parseDouble(value) * 3600 * 1000);
+ }
+ }
+
+ /**
+ * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since
+ * the epoch.
+ *
+ * @param value The attribute value to decode.
+ * @return The parsed timestamp in milliseconds since the epoch.
+ * @throws ParserException if an error occurs parsing the dateTime attribute value.
+ */
+ public static long parseXsDateTime(String value) throws ParserException {
+ Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);
+ if (!matcher.matches()) {
+ throw new ParserException("Invalid date/time format: " + value);
+ }
+
+ int timezoneShift;
+ if (matcher.group(9) == null) {
+ // No time zone specified.
+ timezoneShift = 0;
+ } else if (matcher.group(9).equalsIgnoreCase("Z")) {
+ timezoneShift = 0;
+ } else {
+ timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60
+ + Integer.parseInt(matcher.group(13))));
+ if (matcher.group(11).equals("-")) {
+ timezoneShift *= -1;
+ }
+ }
+
+ Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+
+ dateTime.clear();
+ // Note: The month value is 0-based, hence the -1 on group(2)
+ dateTime.set(Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2)) - 1,
+ Integer.parseInt(matcher.group(3)),
+ Integer.parseInt(matcher.group(4)),
+ Integer.parseInt(matcher.group(5)),
+ Integer.parseInt(matcher.group(6)));
+ if (!TextUtils.isEmpty(matcher.group(8))) {
+ final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
+ // we care only for milliseconds, so movePointRight(3)
+ dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
+ }
+
+ long time = dateTime.getTimeInMillis();
+ if (timezoneShift != 0) {
+ time -= timezoneShift * 60000;
+ }
+
+ return time;
+ }
+
+ /**
+ * Scales a large timestamp.
+ * <p>
+ * Logically, scaling consists of a multiplication followed by a division. The actual operations
+ * performed are designed to minimize the probability of overflow.
+ *
+ * @param timestamp The timestamp to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ * @return The scaled timestamp.
+ */
+ public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ return timestamp / divisionFactor;
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ return timestamp * multiplicationFactor;
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ return (long) (timestamp * multiplicationFactor);
+ }
+ }
+
+ /**
+ * Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps.
+ *
+ * @param timestamps The timestamps to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ * @return The scaled timestamps.
+ */
+ public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {
+ long[] scaledTimestamps = new long[timestamps.size()];
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = timestamps.get(i) / divisionFactor;
+ }
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;
+ }
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);
+ }
+ }
+ return scaledTimestamps;
+ }
+
+ /**
+ * Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps.
+ *
+ * @param timestamps The timestamps to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ */
+ public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] /= divisionFactor;
+ }
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] *= multiplicationFactor;
+ }
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] = (long) (timestamps[i] * multiplicationFactor);
+ }
+ }
+ }
+
+ /**
+ * Converts a list of integers to a primitive array.
+ *
+ * @param list A list of integers.
+ * @return The list in array form, or null if the input list was null.
+ */
+ public static int[] toArray(List<Integer> list) {
+ if (list == null) {
+ return null;
+ }
+ int length = list.size();
+ int[] intArray = new int[length];
+ for (int i = 0; i < length; i++) {
+ intArray[i] = list.get(i);
+ }
+ return intArray;
+ }
+
+ /**
+ * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec}
+ * that represents the remainder of the data.
+ *
+ * @param dataSpec The original {@link DataSpec}.
+ * @param bytesLoaded The number of bytes already loaded.
+ * @return A {@link DataSpec} that represents the remainder of the data.
+ */
+ public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) {
+ if (bytesLoaded == 0) {
+ return dataSpec;
+ } else {
+ long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET
+ : dataSpec.length - bytesLoaded;
+ return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength,
+ dataSpec.key, dataSpec.flags);
+ }
+ }
+
+ /**
+ * Returns the integer equal to the big-endian concatenation of the characters in {@code string}
+ * as bytes. The string must be no more than four characters long.
+ *
+ * @param string A string no more than four characters long.
+ */
+ public static int getIntegerCodeForString(String string) {
+ int length = string.length();
+ Assertions.checkArgument(length <= 4);
+ int result = 0;
+ for (int i = 0; i < length; i++) {
+ result <<= 8;
+ result |= string.charAt(i);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a byte array containing values parsed from the hex string provided.
+ *
+ * @param hexString The hex string to convert to bytes.
+ * @return A byte array containing values parsed from the hex string provided.
+ */
+ public static byte[] getBytesFromHexString(String hexString) {
+ byte[] data = new byte[hexString.length() / 2];
+ for (int i = 0; i < data.length; i++) {
+ int stringOffset = i * 2;
+ data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
+ + Character.digit(hexString.charAt(stringOffset + 1), 16));
+ }
+ return data;
+ }
+
+ /**
+ * Returns a string with comma delimited simple names of each object's class.
+ *
+ * @param objects The objects whose simple class names should be comma delimited and returned.
+ * @return A string with comma delimited simple names of each object's class.
+ */
+ public static String getCommaDelimitedSimpleClassNames(Object[] objects) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < objects.length; i++) {
+ stringBuilder.append(objects[i].getClass().getSimpleName());
+ if (i < objects.length - 1) {
+ stringBuilder.append(", ");
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Returns a user agent string based on the given application name and the library version.
+ *
+ * @param context A valid context of the calling application.
+ * @param applicationName String that will be prefix'ed to the generated user agent.
+ * @return A user agent string generated using the applicationName and the library version.
+ */
+ public static String getUserAgent(Context context, String applicationName) {
+ String versionName;
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ versionName = info.versionName;
+ } catch (NameNotFoundException e) {
+ versionName = "?";
+ }
+ return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE
+ + ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
+ }
+
+ /**
+ * Converts a sample bit depth to a corresponding PCM encoding constant.
+ *
+ * @param bitDepth The bit depth. Supported values are 8, 16, 24 and 32.
+ * @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT},
+ * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
+ * {@link C#ENCODING_PCM_32BIT}. If the bit depth is unsupported then
+ * {@link C#ENCODING_INVALID} is returned.
+ */
+ @C.PcmEncoding
+ public static int getPcmEncoding(int bitDepth) {
+ switch (bitDepth) {
+ case 8:
+ return C.ENCODING_PCM_8BIT;
+ case 16:
+ return C.ENCODING_PCM_16BIT;
+ case 24:
+ return C.ENCODING_PCM_24BIT;
+ case 32:
+ return C.ENCODING_PCM_32BIT;
+ default:
+ return C.ENCODING_INVALID;
+ }
+ }
+
+ /**
+ * Returns the frame size for audio with {@code channelCount} channels in the specified encoding.
+ *
+ * @param pcmEncoding The encoding of the audio data.
+ * @param channelCount The channel count.
+ * @return The size of one audio frame in bytes.
+ */
+ public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) {
+ switch (pcmEncoding) {
+ case C.ENCODING_PCM_8BIT:
+ return channelCount;
+ case C.ENCODING_PCM_16BIT:
+ return channelCount * 2;
+ case C.ENCODING_PCM_24BIT:
+ return channelCount * 3;
+ case C.ENCODING_PCM_32BIT:
+ return channelCount * 4;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Makes a best guess to infer the type from a {@link Uri}.
+ *
+ * @param uri The {@link Uri}.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(Uri uri) {
+ String path = uri.getPath();
+ return path == null ? C.TYPE_OTHER : inferContentType(path);
+ }
+
+ /**
+ * Makes a best guess to infer the type from a file name.
+ *
+ * @param fileName Name of the file. It can include the path of the file.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(String fileName) {
+ fileName = fileName.toLowerCase();
+ if (fileName.endsWith(".mpd")) {
+ return C.TYPE_DASH;
+ } else if (fileName.endsWith(".m3u8")) {
+ return C.TYPE_HLS;
+ } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml")
+ || fileName.endsWith(".ism/manifest") || fileName.endsWith(".isml/manifest")) {
+ return C.TYPE_SS;
+ } else {
+ return C.TYPE_OTHER;
+ }
+ }
+
+ /**
+ * Returns the specified millisecond time formatted as a string.
+ *
+ * @param builder The builder that {@code formatter} will write to.
+ * @param formatter The formatter.
+ * @param timeMs The time to format as a string, in milliseconds.
+ * @return The time formatted as a string.
+ */
+ public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) {
+ if (timeMs == C.TIME_UNSET) {
+ timeMs = 0;
+ }
+ long totalSeconds = (timeMs + 500) / 1000;
+ long seconds = totalSeconds % 60;
+ long minutes = (totalSeconds / 60) % 60;
+ long hours = totalSeconds / 3600;
+ builder.setLength(0);
+ return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
+ : formatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+
+ /**
+ * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C}
+ * {@code DEFAULT_*_BUFFER_SIZE} constant.
+ *
+ * @param trackType The track type.
+ * @return The corresponding default buffer size in bytes.
+ */
+ public static int getDefaultBufferSize(int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_DEFAULT:
+ return C.DEFAULT_MUXED_BUFFER_SIZE;
+ case C.TRACK_TYPE_AUDIO:
+ return C.DEFAULT_AUDIO_BUFFER_SIZE;
+ case C.TRACK_TYPE_VIDEO:
+ return C.DEFAULT_VIDEO_BUFFER_SIZE;
+ case C.TRACK_TYPE_TEXT:
+ return C.DEFAULT_TEXT_BUFFER_SIZE;
+ case C.TRACK_TYPE_METADATA:
+ return C.DEFAULT_METADATA_BUFFER_SIZE;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Escapes a string so that it's safe for use as a file or directory name on at least FAT32
+ * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
+ *
+ * <p>For simplicity, this only handles common characters known to be illegal on FAT32:
+ * <, >, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape
+ * character. Escaping is performed in a consistent way so that no collisions occur and
+ * {@link #unescapeFileName(String)} can be used to retrieve the original file name.
+ *
+ * @param fileName File name to be escaped.
+ * @return An escaped file name which will be safe for use on at least FAT32 filesystems.
+ */
+ public static String escapeFileName(String fileName) {
+ int length = fileName.length();
+ int charactersToEscapeCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (shouldEscapeCharacter(fileName.charAt(i))) {
+ charactersToEscapeCount++;
+ }
+ }
+ if (charactersToEscapeCount == 0) {
+ return fileName;
+ }
+
+ int i = 0;
+ StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2);
+ while (charactersToEscapeCount > 0) {
+ char c = fileName.charAt(i++);
+ if (shouldEscapeCharacter(c)) {
+ builder.append('%').append(Integer.toHexString(c));
+ charactersToEscapeCount--;
+ } else {
+ builder.append(c);
+ }
+ }
+ if (i < length) {
+ builder.append(fileName, i, length);
+ }
+ return builder.toString();
+ }
+
+ private static boolean shouldEscapeCharacter(char c) {
+ switch (c) {
+ case '<':
+ case '>':
+ case ':':
+ case '"':
+ case '/':
+ case '\\':
+ case '|':
+ case '?':
+ case '*':
+ case '%':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Unescapes an escaped file or directory name back to its original value.
+ *
+ * <p>See {@link #escapeFileName(String)} for more information.
+ *
+ * @param fileName File name to be unescaped.
+ * @return The original value of the file name before it was escaped, or null if the escaped
+ * fileName seems invalid.
+ */
+ public static String unescapeFileName(String fileName) {
+ int length = fileName.length();
+ int percentCharacterCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (fileName.charAt(i) == '%') {
+ percentCharacterCount++;
+ }
+ }
+ if (percentCharacterCount == 0) {
+ return fileName;
+ }
+
+ int expectedLength = length - percentCharacterCount * 2;
+ StringBuilder builder = new StringBuilder(expectedLength);
+ Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
+ int endOfLastMatch = 0;
+ while (percentCharacterCount > 0 && matcher.find()) {
+ char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
+ builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter);
+ endOfLastMatch = matcher.end();
+ percentCharacterCount--;
+ }
+ if (endOfLastMatch < length) {
+ builder.append(fileName, endOfLastMatch, length);
+ }
+ if (builder.length() != expectedLength) {
+ return null;
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A hacky method that always throws {@code t} even if {@code t} is a checked exception,
+ * and is not declared to be thrown.
+ */
+ public static void sneakyThrow(Throwable t) {
+ Util.<RuntimeException>sneakyThrowInternal(t);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /** Recursively deletes a directory and its content. */
+ public static void recursiveDelete(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ for (File child : fileOrDirectory.listFiles()) {
+ recursiveDelete(child);
+ }
+ }
+ fileOrDirectory.delete();
+ }
+
+ /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */
+ public static File createTempDirectory(Context context, String prefix) throws IOException {
+ File tempFile = File.createTempFile(prefix, null, context.getCacheDir());
+ tempFile.delete(); // Delete the temp file.
+ tempFile.mkdir(); // Create a directory with the same name.
+ return tempFile;
+ }
+
+ /**
+ * Returns the result of updating a CRC with the specified bytes in a "most significant bit first"
+ * order.
+ *
+ * @param bytes Array containing the bytes to update the crc value with.
+ * @param start The index to the first byte in the byte range to update the crc with.
+ * @param end The index after the last byte in the byte range to update the crc with.
+ * @param initialValue The initial value for the crc calculation.
+ * @return The result of updating the initial value with the specified bytes.
+ */
+ public static int crc(byte[] bytes, int start, int end, int initialValue) {
+ for (int i = start; i < end; i++) {
+ initialValue = (initialValue << 8)
+ ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF];
+ }
+ return initialValue;
+ }
+
+ /**
+ * Gets the physical size of the default display, in pixels.
+ *
+ * @param context Any context.
+ * @return The physical display size, in pixels.
+ */
+ public static Point getPhysicalDisplaySize(Context context) {
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ return getPhysicalDisplaySize(context, windowManager.getDefaultDisplay());
+ }
+
+ /**
+ * Gets the physical size of the specified display, in pixels.
+ *
+ * @param context Any context.
+ * @param display The display whose size is to be returned.
+ * @return The physical display size, in pixels.
+ */
+ public static Point getPhysicalDisplaySize(Context context, Display display) {
+ if (Util.SDK_INT < 25 && display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ // Before API 25 the Display object does not provide a working way to identify Android TVs
+ // that can show 4k resolution in a SurfaceView, so check for supported devices here.
+ if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL.startsWith("BRAVIA")
+ && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
+ return new Point(3840, 2160);
+ } else if ("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL.contains("SHIELD")) {
+ // Attempt to read sys.display-size.
+ String sysDisplaySize = null;
+ try {
+ Class<?> systemProperties = Class.forName("android.os.SystemProperties");
+ Method getMethod = systemProperties.getMethod("get", String.class);
+ sysDisplaySize = (String) getMethod.invoke(systemProperties, "sys.display-size");
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to read sys.display-size", e);
+ }
+ // If we managed to read sys.display-size, attempt to parse it.
+ if (!TextUtils.isEmpty(sysDisplaySize)) {
+ try {
+ String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x");
+ if (sysDisplaySizeParts.length == 2) {
+ int width = Integer.parseInt(sysDisplaySizeParts[0]);
+ int height = Integer.parseInt(sysDisplaySizeParts[1]);
+ if (width > 0 && height > 0) {
+ return new Point(width, height);
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Do nothing.
+ }
+ Log.e(TAG, "Invalid sys.display-size: " + sysDisplaySize);
+ }
+ }
+ }
+
+ Point displaySize = new Point();
+ if (Util.SDK_INT >= 23) {
+ getDisplaySizeV23(display, displaySize);
+ } else if (Util.SDK_INT >= 17) {
+ getDisplaySizeV17(display, displaySize);
+ } else if (Util.SDK_INT >= 16) {
+ getDisplaySizeV16(display, displaySize);
+ } else {
+ getDisplaySizeV9(display, displaySize);
+ }
+ return displaySize;
+ }
+
+ @TargetApi(23)
+ private static void getDisplaySizeV23(Display display, Point outSize) {
+ Display.Mode mode = display.getMode();
+ outSize.x = mode.getPhysicalWidth();
+ outSize.y = mode.getPhysicalHeight();
+ }
+
+ @TargetApi(17)
+ private static void getDisplaySizeV17(Display display, Point outSize) {
+ display.getRealSize(outSize);
+ }
+
+ @TargetApi(16)
+ private static void getDisplaySizeV16(Display display, Point outSize) {
+ display.getSize(outSize);
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void getDisplaySizeV9(Display display, Point outSize) {
+ outSize.x = display.getWidth();
+ outSize.y = display.getHeight();
+ }
+
+ /**
+ * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order
+ * "most significant bit first".
+ */
+ private static final int[] CRC32_BYTES_MSBF = {
+ 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2,
+ 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3,
+ 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC,
+ 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011,
+ 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E,
+ 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF,
+ 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90,
+ 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95,
+ 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A,
+ 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C,
+ 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13,
+ 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE,
+ 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1,
+ 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20,
+ 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F,
+ 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A,
+ 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055,
+ 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34,
+ 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632,
+ 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F,
+ 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0,
+ 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91,
+ 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E,
+ 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B,
+ 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604,
+ 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615,
+ 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A,
+ 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640,
+ 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F,
+ 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E,
+ 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651,
+ 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654,
+ 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB,
+ 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA,
+ 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5,
+ 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668,
+ 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/util/XmlPullParserUtil.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 com.google.android.exoplayer2.util;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * {@link XmlPullParser} utility methods.
+ */
+public final class XmlPullParserUtil {
+
+ private XmlPullParserUtil() {}
+
+ /**
+ * Returns whether the current event is an end tag with the specified name.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param name The specified name.
+ * @return Whether the current event is an end tag with the specified name.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return isEndTag(xpp) && xpp.getName().equals(name);
+ }
+
+ /**
+ * Returns whether the current event is an end tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @return Whether the current event is an end tag.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.END_TAG;
+ }
+
+ /**
+ * Returns whether the current event is a start tag with the specified name.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param name The specified name.
+ * @return Whether the current event is a start tag with the specified name.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isStartTag(XmlPullParser xpp, String name)
+ throws XmlPullParserException {
+ return isStartTag(xpp) && xpp.getName().equals(name);
+ }
+
+ /**
+ * Returns whether the current event is a start tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @return Whether the current event is a start tag.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.START_TAG;
+ }
+
+ /**
+ * Returns the value of an attribute of the current start tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param attributeName The name of the attribute.
+ * @return The value of the attribute, or null if the current event is not a start tag or if no
+ * no such attribute was found.
+ */
+ public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
+ int attributeCount = xpp.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ if (attributeName.equals(xpp.getAttributeName(i))) {
+ return xpp.getAttributeValue(i);
+ }
+ }
+ return null;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/video/AvcConfig.java
@@ -0,0 +1,97 @@
+/*
+ * 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 com.google.android.exoplayer2.video;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AVC configuration data.
+ */
+public final class AvcConfig {
+
+ public final List<byte[]> initializationData;
+ public final int nalUnitLengthFieldLength;
+ public final int width;
+ public final int height;
+ public final float pixelWidthAspectRatio;
+
+ /**
+ * Parses AVC configuration data.
+ *
+ * @param data A {@link ParsableByteArray}, whose position is set to the start of the AVC
+ * configuration data to parse.
+ * @return A parsed representation of the HEVC configuration data.
+ * @throws ParserException If an error occurred parsing the data.
+ */
+ public static AvcConfig parse(ParsableByteArray data) throws ParserException {
+ try {
+ data.skipBytes(4); // Skip to the AVCDecoderConfigurationRecord (defined in 14496-15)
+ int nalUnitLengthFieldLength = (data.readUnsignedByte() & 0x3) + 1;
+ if (nalUnitLengthFieldLength == 3) {
+ throw new IllegalStateException();
+ }
+ List<byte[]> initializationData = new ArrayList<>();
+ int numSequenceParameterSets = data.readUnsignedByte() & 0x1F;
+ for (int j = 0; j < numSequenceParameterSets; j++) {
+ initializationData.add(buildNalUnitForChild(data));
+ }
+ int numPictureParameterSets = data.readUnsignedByte();
+ for (int j = 0; j < numPictureParameterSets; j++) {
+ initializationData.add(buildNalUnitForChild(data));
+ }
+
+ int width = Format.NO_VALUE;
+ int height = Format.NO_VALUE;
+ float pixelWidthAspectRatio = 1;
+ if (numSequenceParameterSets > 0) {
+ byte[] sps = initializationData.get(0);
+ SpsData spsData = NalUnitUtil.parseSpsNalUnit(initializationData.get(0),
+ nalUnitLengthFieldLength, sps.length);
+ width = spsData.width;
+ height = spsData.height;
+ pixelWidthAspectRatio = spsData.pixelWidthAspectRatio;
+ }
+ return new AvcConfig(initializationData, nalUnitLengthFieldLength, width, height,
+ pixelWidthAspectRatio);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing AVC config", e);
+ }
+ }
+
+ private AvcConfig(List<byte[]> initializationData, int nalUnitLengthFieldLength,
+ int width, int height, float pixelWidthAspectRatio) {
+ this.initializationData = initializationData;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ this.width = width;
+ this.height = height;
+ this.pixelWidthAspectRatio = pixelWidthAspectRatio;
+ }
+
+ private static byte[] buildNalUnitForChild(ParsableByteArray data) {
+ int length = data.readUnsignedShort();
+ int offset = data.getPosition();
+ data.skipBytes(length);
+ return CodecSpecificDataUtil.buildNalUnit(data.data, offset, length);
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/video/ColorInfo.java
@@ -0,0 +1,149 @@
+/*
+ * 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 com.google.android.exoplayer2.video;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import java.util.Arrays;
+
+/**
+ * Stores color info.
+ */
+public final class ColorInfo implements Parcelable {
+
+ /**
+ * The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link
+ * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.
+ */
+ @C.ColorSpace
+ public final int colorSpace;
+
+ /**
+ * The color range of the video. Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link
+ * C#COLOR_RANGE_FULL} or {@link Format#NO_VALUE} if unknown.
+ */
+ @C.ColorRange
+ public final int colorRange;
+
+ /**
+ * The color transfer characteristicks of the video. Valid values are {@link
+ * C#COLOR_TRANSFER_HLG}, {@link C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link
+ * Format#NO_VALUE} if unknown.
+ */
+ @C.ColorTransfer
+ public final int colorTransfer;
+
+ /**
+ * HdrStaticInfo as defined in CTA-861.3.
+ */
+ public final byte[] hdrStaticInfo;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * Constructs the ColorInfo.
+ *
+ * @param colorSpace The color space of the video.
+ * @param colorRange The color range of the video.
+ * @param colorTransfer The color transfer characteristics of the video.
+ * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3.
+ */
+ public ColorInfo(@C.ColorSpace int colorSpace, @C.ColorRange int colorRange,
+ @C.ColorTransfer int colorTransfer, byte[] hdrStaticInfo) {
+ this.colorSpace = colorSpace;
+ this.colorRange = colorRange;
+ this.colorTransfer = colorTransfer;
+ this.hdrStaticInfo = hdrStaticInfo;
+ }
+
+ @SuppressWarnings("ResourceType")
+ /* package */ ColorInfo(Parcel in) {
+ colorSpace = in.readInt();
+ colorRange = in.readInt();
+ colorTransfer = in.readInt();
+ boolean hasHdrStaticInfo = in.readInt() != 0;
+ hdrStaticInfo = hasHdrStaticInfo ? in.createByteArray() : null;
+ }
+
+ // Parcelable implementation.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ColorInfo other = (ColorInfo) obj;
+ if (colorSpace != other.colorSpace || colorRange != other.colorRange
+ || colorTransfer != other.colorTransfer
+ || !Arrays.equals(hdrStaticInfo, other.hdrStaticInfo)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "ColorInfo(" + colorSpace + ", " + colorRange + ", " + colorTransfer
+ + ", " + (hdrStaticInfo != null) + ")";
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + colorSpace;
+ result = 31 * result + colorRange;
+ result = 31 * result + colorTransfer;
+ result = 31 * result + Arrays.hashCode(hdrStaticInfo);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(colorSpace);
+ dest.writeInt(colorRange);
+ dest.writeInt(colorTransfer);
+ dest.writeInt(hdrStaticInfo != null ? 1 : 0);
+ if (hdrStaticInfo != null) {
+ dest.writeByteArray(hdrStaticInfo);
+ }
+ }
+
+ public static final Parcelable.Creator<ColorInfo> CREATOR = new Parcelable.Creator<ColorInfo>() {
+ @Override
+ public ColorInfo createFromParcel(Parcel in) {
+ return new ColorInfo(in);
+ }
+
+ @Override
+ public ColorInfo[] newArray(int size) {
+ return new ColorInfo[0];
+ }
+ };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/video/HevcConfig.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.google.android.exoplayer2.video;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * HEVC configuration data.
+ */
+public final class HevcConfig {
+
+ public final List<byte[]> initializationData;
+ public final int nalUnitLengthFieldLength;
+
+ /**
+ * Parses HEVC configuration data.
+ *
+ * @param data A {@link ParsableByteArray}, whose position is set to the start of the HEVC
+ * configuration data to parse.
+ * @return A parsed representation of the HEVC configuration data.
+ * @throws ParserException If an error occurred parsing the data.
+ */
+ public static HevcConfig parse(ParsableByteArray data) throws ParserException {
+ try {
+ data.skipBytes(21); // Skip to the NAL unit length size field.
+ int lengthSizeMinusOne = data.readUnsignedByte() & 0x03;
+
+ // Calculate the combined size of all VPS/SPS/PPS bitstreams.
+ int numberOfArrays = data.readUnsignedByte();
+ int csdLength = 0;
+ int csdStartPosition = data.getPosition();
+ for (int i = 0; i < numberOfArrays; i++) {
+ data.skipBytes(1); // completeness (1), nal_unit_type (7)
+ int numberOfNalUnits = data.readUnsignedShort();
+ for (int j = 0; j < numberOfNalUnits; j++) {
+ int nalUnitLength = data.readUnsignedShort();
+ csdLength += 4 + nalUnitLength; // Start code and NAL unit.
+ data.skipBytes(nalUnitLength);
+ }
+ }
+
+ // Concatenate the codec-specific data into a single buffer.
+ data.setPosition(csdStartPosition);
+ byte[] buffer = new byte[csdLength];
+ int bufferPosition = 0;
+ for (int i = 0; i < numberOfArrays; i++) {
+ data.skipBytes(1); // completeness (1), nal_unit_type (7)
+ int numberOfNalUnits = data.readUnsignedShort();
+ for (int j = 0; j < numberOfNalUnits; j++) {
+ int nalUnitLength = data.readUnsignedShort();
+ System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition,
+ NalUnitUtil.NAL_START_CODE.length);
+ bufferPosition += NalUnitUtil.NAL_START_CODE.length;
+ System
+ .arraycopy(data.data, data.getPosition(), buffer, bufferPosition, nalUnitLength);
+ bufferPosition += nalUnitLength;
+ data.skipBytes(nalUnitLength);
+ }
+ }
+
+ List<byte[]> initializationData = csdLength == 0 ? null : Collections.singletonList(buffer);
+ return new HevcConfig(initializationData, lengthSizeMinusOne + 1);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ParserException("Error parsing HEVC config", e);
+ }
+ }
+
+ private HevcConfig(List<byte[]> initializationData, int nalUnitLengthFieldLength) {
+ this.initializationData = initializationData;
+ this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -0,0 +1,873 @@
+/*
+ * 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 com.google.android.exoplayer2.video;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Point;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders video using {@link MediaCodec}.
+ */
+@TargetApi(16)
+public class MediaCodecVideoRenderer extends MediaCodecRenderer {
+
+ private static final String TAG = "MediaCodecVideoRenderer";
+ private static final String KEY_CROP_LEFT = "crop-left";
+ private static final String KEY_CROP_RIGHT = "crop-right";
+ private static final String KEY_CROP_BOTTOM = "crop-bottom";
+ private static final String KEY_CROP_TOP = "crop-top";
+
+ // Long edge length in pixels for standard video formats, in decreasing in order.
+ private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] {
+ 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480};
+
+ private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
+ private final EventDispatcher eventDispatcher;
+ private final long allowedJoiningTimeMs;
+ private final int maxDroppedFramesToNotify;
+ private final boolean deviceNeedsAutoFrcWorkaround;
+
+ private Format[] streamFormats;
+ private CodecMaxValues codecMaxValues;
+
+ private Surface surface;
+ @C.VideoScalingMode
+ private int scalingMode;
+ private boolean renderedFirstFrame;
+ private long joiningDeadlineMs;
+ private long droppedFrameAccumulationStartTimeMs;
+ private int droppedFrames;
+ private int consecutiveDroppedFrameCount;
+
+ private int pendingRotationDegrees;
+ private float pendingPixelWidthHeightRatio;
+ private int currentWidth;
+ private int currentHeight;
+ private int currentUnappliedRotationDegrees;
+ private float currentPixelWidthHeightRatio;
+ private int reportedWidth;
+ private int reportedHeight;
+ private int reportedUnappliedRotationDegrees;
+ private float reportedPixelWidthHeightRatio;
+
+ private boolean tunneling;
+ private int tunnelingAudioSessionId;
+ /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
+
+ /**
+ * @param context A context.
+ * @param mediaCodecSelector A decoder selector.
+ */
+ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
+ this(context, mediaCodecSelector, 0);
+ }
+
+ /**
+ * @param context A context.
+ * @param mediaCodecSelector A decoder selector.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ */
+ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
+ long allowedJoiningTimeMs) {
+ this(context, mediaCodecSelector, allowedJoiningTimeMs, null, null, -1);
+ }
+
+ /**
+ * @param context A context.
+ * @param mediaCodecSelector A decoder selector.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
+ * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+ */
+ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
+ long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener,
+ int maxDroppedFrameCountToNotify) {
+ this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler,
+ eventListener, maxDroppedFrameCountToNotify);
+ }
+
+ /**
+ * @param context A context.
+ * @param mediaCodecSelector A decoder selector.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisition. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
+ * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+ */
+ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
+ long allowedJoiningTimeMs, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys, Handler eventHandler,
+ VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {
+ super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
+ this.allowedJoiningTimeMs = allowedJoiningTimeMs;
+ this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
+ frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
+ joiningDeadlineMs = C.TIME_UNSET;
+ currentWidth = Format.NO_VALUE;
+ currentHeight = Format.NO_VALUE;
+ currentPixelWidthHeightRatio = Format.NO_VALUE;
+ pendingPixelWidthHeightRatio = Format.NO_VALUE;
+ scalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+ clearReportedVideoSize();
+ }
+
+ @Override
+ protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
+ throws DecoderQueryException {
+ String mimeType = format.sampleMimeType;
+ if (!MimeTypes.isVideo(mimeType)) {
+ return FORMAT_UNSUPPORTED_TYPE;
+ }
+ boolean requiresSecureDecryption = false;
+ DrmInitData drmInitData = format.drmInitData;
+ if (drmInitData != null) {
+ for (int i = 0; i < drmInitData.schemeDataCount; i++) {
+ requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;
+ }
+ }
+ MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType,
+ requiresSecureDecryption);
+ if (decoderInfo == null) {
+ return FORMAT_UNSUPPORTED_SUBTYPE;
+ }
+
+ boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs);
+ if (decoderCapable && format.width > 0 && format.height > 0) {
+ if (Util.SDK_INT >= 21) {
+ decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height,
+ format.frameRate);
+ } else {
+ decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize();
+ if (!decoderCapable) {
+ Log.d(TAG, "FalseCheck [legacyFrameSize, " + format.width + "x" + format.height + "] ["
+ + Util.DEVICE_DEBUG_INFO + "]");
+ }
+ }
+ }
+
+ int adaptiveSupport = decoderInfo.adaptive ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS;
+ int tunnelingSupport = decoderInfo.tunneling ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+ int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
+ return adaptiveSupport | tunnelingSupport | formatSupport;
+ }
+
+ @Override
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ super.onEnabled(joining);
+ tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+ tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET;
+ eventDispatcher.enabled(decoderCounters);
+ frameReleaseTimeHelper.enable();
+ }
+
+ @Override
+ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ streamFormats = formats;
+ super.onStreamChanged(formats);
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ super.onPositionReset(positionUs, joining);
+ clearRenderedFirstFrame();
+ consecutiveDroppedFrameCount = 0;
+ if (joining) {
+ setJoiningDeadlineMs();
+ } else {
+ joiningDeadlineMs = C.TIME_UNSET;
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) {
+ // Ready. If we were joining then we've now joined, so clear the joining deadline.
+ joiningDeadlineMs = C.TIME_UNSET;
+ return true;
+ } else if (joiningDeadlineMs == C.TIME_UNSET) {
+ // Not joining.
+ return false;
+ } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) {
+ // Joining and still within the joining deadline.
+ return true;
+ } else {
+ // The joining deadline has been exceeded. Give up and clear the deadline.
+ joiningDeadlineMs = C.TIME_UNSET;
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ super.onStarted();
+ droppedFrames = 0;
+ droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
+ joiningDeadlineMs = C.TIME_UNSET;
+ }
+
+ @Override
+ protected void onStopped() {
+ maybeNotifyDroppedFrames();
+ super.onStopped();
+ }
+
+ @Override
+ protected void onDisabled() {
+ currentWidth = Format.NO_VALUE;
+ currentHeight = Format.NO_VALUE;
+ currentPixelWidthHeightRatio = Format.NO_VALUE;
+ pendingPixelWidthHeightRatio = Format.NO_VALUE;
+ clearReportedVideoSize();
+ clearRenderedFirstFrame();
+ frameReleaseTimeHelper.disable();
+ tunnelingOnFrameRenderedListener = null;
+ try {
+ super.onDisabled();
+ } finally {
+ decoderCounters.ensureUpdated();
+ eventDispatcher.disabled(decoderCounters);
+ }
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ if (messageType == C.MSG_SET_SURFACE) {
+ setSurface((Surface) message);
+ } else if (messageType == C.MSG_SET_SCALING_MODE) {
+ scalingMode = (Integer) message;
+ MediaCodec codec = getCodec();
+ if (codec != null) {
+ setVideoScalingMode(codec, scalingMode);
+ }
+ } else {
+ super.handleMessage(messageType, message);
+ }
+ }
+
+ private void setSurface(Surface surface) throws ExoPlaybackException {
+ // We only need to update the codec if the surface has changed.
+ if (this.surface != surface) {
+ this.surface = surface;
+ int state = getState();
+ if (state == STATE_ENABLED || state == STATE_STARTED) {
+ MediaCodec codec = getCodec();
+ if (Util.SDK_INT >= 23 && codec != null && surface != null) {
+ setOutputSurfaceV23(codec, surface);
+ } else {
+ releaseCodec();
+ maybeInitCodec();
+ }
+ }
+ if (surface != null) {
+ // If we know the video size, report it again immediately.
+ maybeRenotifyVideoSizeChanged();
+ // We haven't rendered to the new surface yet.
+ clearRenderedFirstFrame();
+ if (state == STATE_STARTED) {
+ setJoiningDeadlineMs();
+ }
+ } else {
+ // The surface has been removed.
+ clearReportedVideoSize();
+ clearRenderedFirstFrame();
+ }
+ } else if (surface != null) {
+ // The surface is unchanged and non-null. If we know the video size and/or have already
+ // rendered to the surface, report these again immediately.
+ maybeRenotifyVideoSizeChanged();
+ maybeRenotifyRenderedFirstFrame();
+ }
+ }
+
+ @Override
+ protected boolean shouldInitCodec() {
+ return super.shouldInitCodec() && surface != null && surface.isValid();
+ }
+
+ @Override
+ protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
+ MediaCrypto crypto) throws DecoderQueryException {
+ codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats);
+ MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
+ tunnelingAudioSessionId);
+ codec.configure(mediaFormat, surface, crypto, 0);
+ if (Util.SDK_INT >= 23 && tunneling) {
+ tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
+ }
+ }
+
+ @Override
+ protected void onCodecInitialized(String name, long initializedTimestampMs,
+ long initializationDurationMs) {
+ eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
+ }
+
+ @Override
+ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ super.onInputFormatChanged(newFormat);
+ eventDispatcher.inputFormatChanged(newFormat);
+ pendingPixelWidthHeightRatio = getPixelWidthHeightRatio(newFormat);
+ pendingRotationDegrees = getRotationDegrees(newFormat);
+ }
+
+ @Override
+ protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
+ if (Util.SDK_INT < 23 && tunneling) {
+ maybeNotifyRenderedFirstFrame();
+ }
+ }
+
+ @Override
+ protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) {
+ boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
+ && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
+ && outputFormat.containsKey(KEY_CROP_TOP);
+ currentWidth = hasCrop
+ ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1
+ : outputFormat.getInteger(MediaFormat.KEY_WIDTH);
+ currentHeight = hasCrop
+ ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
+ : outputFormat.getInteger(MediaFormat.KEY_HEIGHT);
+ currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio;
+ if (Util.SDK_INT >= 21) {
+ // On API level 21 and above the decoder applies the rotation when rendering to the surface.
+ // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need
+ // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied.
+ if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) {
+ int rotatedHeight = currentWidth;
+ currentWidth = currentHeight;
+ currentHeight = rotatedHeight;
+ currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio;
+ }
+ } else {
+ // On API level 20 and below the decoder does not apply the rotation.
+ currentUnappliedRotationDegrees = pendingRotationDegrees;
+ }
+ // Must be applied each time the output format changes.
+ setVideoScalingMode(codec, scalingMode);
+ }
+
+ @Override
+ protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
+ Format oldFormat, Format newFormat) {
+ return areAdaptationCompatible(oldFormat, newFormat)
+ && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height
+ && newFormat.maxInputSize <= codecMaxValues.inputSize
+ && (codecIsAdaptive
+ || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
+ }
+
+ @Override
+ protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
+ ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
+ boolean shouldSkip) {
+ if (shouldSkip) {
+ skipOutputBuffer(codec, bufferIndex);
+ return true;
+ }
+
+ if (!renderedFirstFrame) {
+ if (Util.SDK_INT >= 21) {
+ renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
+ } else {
+ renderOutputBuffer(codec, bufferIndex);
+ }
+ return true;
+ }
+
+ if (getState() != STATE_STARTED) {
+ return false;
+ }
+
+ // Compute how many microseconds it is until the buffer's presentation time.
+ long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
+ long earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
+
+ // Compute the buffer's desired release time in nanoseconds.
+ long systemTimeNs = System.nanoTime();
+ long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
+
+ // Apply a timestamp adjustment, if there is one.
+ long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(
+ bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs);
+ earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
+
+ if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
+ // We're more than 30ms late rendering the frame.
+ dropOutputBuffer(codec, bufferIndex);
+ return true;
+ }
+
+ if (Util.SDK_INT >= 21) {
+ // Let the underlying framework time the release.
+ if (earlyUs < 50000) {
+ renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);
+ return true;
+ }
+ } else {
+ // We need to time the release ourselves.
+ if (earlyUs < 30000) {
+ if (earlyUs > 11000) {
+ // We're a little too early to render the frame. Sleep until the frame can be rendered.
+ // Note: The 11ms threshold was chosen fairly arbitrarily.
+ try {
+ // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.
+ Thread.sleep((earlyUs - 10000) / 1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ renderOutputBuffer(codec, bufferIndex);
+ return true;
+ }
+ }
+
+ // We're either not playing, or it's not time to render the frame yet.
+ return false;
+ }
+
+ /**
+ * Returns whether the buffer being processed should be dropped.
+ *
+ * @param earlyUs The time until the buffer should be presented in microseconds. A negative value
+ * indicates that the buffer is late.
+ * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
+ * measured at the start of the current iteration of the rendering loop.
+ */
+ protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
+ // Drop the frame if we're more than 30ms late rendering the frame.
+ return earlyUs < -30000;
+ }
+
+ private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
+ TraceUtil.beginSection("skipVideoBuffer");
+ codec.releaseOutputBuffer(bufferIndex, false);
+ TraceUtil.endSection();
+ decoderCounters.skippedOutputBufferCount++;
+ }
+
+ private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
+ TraceUtil.beginSection("dropVideoBuffer");
+ codec.releaseOutputBuffer(bufferIndex, false);
+ TraceUtil.endSection();
+ decoderCounters.droppedOutputBufferCount++;
+ droppedFrames++;
+ consecutiveDroppedFrameCount++;
+ decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount,
+ decoderCounters.maxConsecutiveDroppedOutputBufferCount);
+ if (droppedFrames == maxDroppedFramesToNotify) {
+ maybeNotifyDroppedFrames();
+ }
+ }
+
+ private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
+ maybeNotifyVideoSizeChanged();
+ TraceUtil.beginSection("releaseOutputBuffer");
+ codec.releaseOutputBuffer(bufferIndex, true);
+ TraceUtil.endSection();
+ decoderCounters.renderedOutputBufferCount++;
+ consecutiveDroppedFrameCount = 0;
+ maybeNotifyRenderedFirstFrame();
+ }
+
+ @TargetApi(21)
+ private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
+ maybeNotifyVideoSizeChanged();
+ TraceUtil.beginSection("releaseOutputBuffer");
+ codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
+ TraceUtil.endSection();
+ decoderCounters.renderedOutputBufferCount++;
+ consecutiveDroppedFrameCount = 0;
+ maybeNotifyRenderedFirstFrame();
+ }
+
+ private void setJoiningDeadlineMs() {
+ joiningDeadlineMs = allowedJoiningTimeMs > 0
+ ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
+ }
+
+ private void clearRenderedFirstFrame() {
+ renderedFirstFrame = false;
+ // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for
+ // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and
+ // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and
+ // above.
+ if (Util.SDK_INT >= 23 && tunneling) {
+ MediaCodec codec = getCodec();
+ // If codec is null then the listener will be instantiated in configureCodec.
+ if (codec != null) {
+ tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
+ }
+ }
+ }
+
+ /* package */ void maybeNotifyRenderedFirstFrame() {
+ if (!renderedFirstFrame) {
+ renderedFirstFrame = true;
+ eventDispatcher.renderedFirstFrame(surface);
+ }
+ }
+
+ private void maybeRenotifyRenderedFirstFrame() {
+ if (renderedFirstFrame) {
+ eventDispatcher.renderedFirstFrame(surface);
+ }
+ }
+
+ private void clearReportedVideoSize() {
+ reportedWidth = Format.NO_VALUE;
+ reportedHeight = Format.NO_VALUE;
+ reportedPixelWidthHeightRatio = Format.NO_VALUE;
+ reportedUnappliedRotationDegrees = Format.NO_VALUE;
+ }
+
+ private void maybeNotifyVideoSizeChanged() {
+ if (reportedWidth != currentWidth || reportedHeight != currentHeight
+ || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees
+ || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) {
+ eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,
+ currentPixelWidthHeightRatio);
+ reportedWidth = currentWidth;
+ reportedHeight = currentHeight;
+ reportedUnappliedRotationDegrees = currentUnappliedRotationDegrees;
+ reportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
+ }
+ }
+
+ private void maybeRenotifyVideoSizeChanged() {
+ if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {
+ eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,
+ currentPixelWidthHeightRatio);
+ }
+ }
+
+ private void maybeNotifyDroppedFrames() {
+ if (droppedFrames > 0) {
+ long now = SystemClock.elapsedRealtime();
+ long elapsedMs = now - droppedFrameAccumulationStartTimeMs;
+ eventDispatcher.droppedFrames(droppedFrames, elapsedMs);
+ droppedFrames = 0;
+ droppedFrameAccumulationStartTimeMs = now;
+ }
+ }
+
+ @SuppressLint("InlinedApi")
+ private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues,
+ boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) {
+ MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16();
+ // Set the maximum adaptive video dimensions.
+ frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width);
+ frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height);
+ // Set the maximum input size.
+ if (codecMaxValues.inputSize != Format.NO_VALUE) {
+ frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize);
+ }
+ // Set FRC workaround.
+ if (deviceNeedsAutoFrcWorkaround) {
+ frameworkMediaFormat.setInteger("auto-frc", 0);
+ }
+ // Configure tunneling if enabled.
+ if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId);
+ }
+ return frameworkMediaFormat;
+ }
+
+ @TargetApi(23)
+ private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) {
+ codec.setOutputSurface(surface);
+ }
+
+ @TargetApi(21)
+ private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) {
+ mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true);
+ mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId);
+ }
+
+ /**
+ * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way
+ * that will allow possible adaptation to other compatible formats in {@code streamFormats}.
+ *
+ * @param codecInfo Information about the {@link MediaCodec} being configured.
+ * @param format The format for which the codec is being configured.
+ * @param streamFormats The possible stream formats.
+ * @return Suitable {@link CodecMaxValues}.
+ * @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
+ */
+ private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format,
+ Format[] streamFormats) throws DecoderQueryException {
+ int maxWidth = format.width;
+ int maxHeight = format.height;
+ int maxInputSize = getMaxInputSize(format);
+ if (streamFormats.length == 1) {
+ // The single entry in streamFormats must correspond to the format for which the codec is
+ // being configured.
+ return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);
+ }
+ boolean haveUnknownDimensions = false;
+ for (Format streamFormat : streamFormats) {
+ if (areAdaptationCompatible(format, streamFormat)) {
+ haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE
+ || streamFormat.height == Format.NO_VALUE);
+ maxWidth = Math.max(maxWidth, streamFormat.width);
+ maxHeight = Math.max(maxHeight, streamFormat.height);
+ maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat));
+ }
+ }
+ if (haveUnknownDimensions) {
+ Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight);
+ Point codecMaxSize = getCodecMaxSize(codecInfo, format);
+ if (codecMaxSize != null) {
+ maxWidth = Math.max(maxWidth, codecMaxSize.x);
+ maxHeight = Math.max(maxHeight, codecMaxSize.y);
+ maxInputSize = Math.max(maxInputSize,
+ getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight));
+ Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight);
+ }
+ }
+ return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);
+ }
+
+ /**
+ * Returns a maximum video size to use when configuring a codec for {@code format} in a way
+ * that will allow possible adaptation to other compatible formats that are expected to have the
+ * same aspect ratio, but whose sizes are unknown.
+ *
+ * @param codecInfo Information about the {@link MediaCodec} being configured.
+ * @param format The format for which the codec is being configured.
+ * @return The maximum video size to use, or null if the size of {@code format} should be used.
+ * @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
+ */
+ private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format)
+ throws DecoderQueryException {
+ boolean isVerticalVideo = format.height > format.width;
+ int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
+ int formatShortEdgePx = isVerticalVideo ? format.width : format.height;
+ float aspectRatio = (float) formatShortEdgePx / formatLongEdgePx;
+ for (int longEdgePx : STANDARD_LONG_EDGE_VIDEO_PX) {
+ int shortEdgePx = (int) (longEdgePx * aspectRatio);
+ if (longEdgePx <= formatLongEdgePx || shortEdgePx <= formatShortEdgePx) {
+ // Don't return a size not larger than the format for which the codec is being configured.
+ return null;
+ } else if (Util.SDK_INT >= 21) {
+ Point alignedSize = codecInfo.alignVideoSizeV21(isVerticalVideo ? shortEdgePx : longEdgePx,
+ isVerticalVideo ? longEdgePx : shortEdgePx);
+ float frameRate = format.frameRate;
+ if (codecInfo.isVideoSizeAndRateSupportedV21(alignedSize.x, alignedSize.y, frameRate)) {
+ return alignedSize;
+ }
+ } else {
+ // Conservatively assume the codec requires 16px width and height alignment.
+ longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16;
+ shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16;
+ if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) {
+ return new Point(isVerticalVideo ? shortEdgePx : longEdgePx,
+ isVerticalVideo ? longEdgePx : shortEdgePx);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a maximum input size for a given format.
+ *
+ * @param format The format.
+ * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
+ * determined.
+ */
+ private static int getMaxInputSize(Format format) {
+ if (format.maxInputSize != Format.NO_VALUE) {
+ // The format defines an explicit maximum input size.
+ return format.maxInputSize;
+ }
+ return getMaxInputSize(format.sampleMimeType, format.width, format.height);
+ }
+
+ /**
+ * Returns a maximum input size for a given mime type, width and height.
+ *
+ * @param sampleMimeType The format mime type.
+ * @param width The width in pixels.
+ * @param height The height in pixels.
+ * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
+ * determined.
+ */
+ private static int getMaxInputSize(String sampleMimeType, int width, int height) {
+ if (width == Format.NO_VALUE || height == Format.NO_VALUE) {
+ // We can't infer a maximum input size without video dimensions.
+ return Format.NO_VALUE;
+ }
+
+ // Attempt to infer a maximum input size from the format.
+ int maxPixels;
+ int minCompressionRatio;
+ switch (sampleMimeType) {
+ case MimeTypes.VIDEO_H263:
+ case MimeTypes.VIDEO_MP4V:
+ maxPixels = width * height;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_H264:
+ if ("BRAVIA 4K 2015".equals(Util.MODEL)) {
+ // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video
+ // maximum input size, so use the default value.
+ return Format.NO_VALUE;
+ }
+ // Round up width/height to an integer number of macroblocks.
+ maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_VP8:
+ // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
+ maxPixels = width * height;
+ minCompressionRatio = 2;
+ break;
+ case MimeTypes.VIDEO_H265:
+ case MimeTypes.VIDEO_VP9:
+ maxPixels = width * height;
+ minCompressionRatio = 4;
+ break;
+ default:
+ // Leave the default max input size.
+ return Format.NO_VALUE;
+ }
+ // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
+ return (maxPixels * 3) / (2 * minCompressionRatio);
+ }
+
+ private static void setVideoScalingMode(MediaCodec codec, int scalingMode) {
+ codec.setVideoScalingMode(scalingMode);
+ }
+
+ /**
+ * Returns whether the device is known to enable frame-rate conversion logic that negatively
+ * impacts ExoPlayer.
+ * <p>
+ * If true is returned then we explicitly disable the feature.
+ *
+ * @return True if the device is known to enable frame-rate conversion logic that negatively
+ * impacts ExoPlayer. False otherwise.
+ */
+ private static boolean deviceNeedsAutoFrcWorkaround() {
+ // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of
+ // content to the refresh rate of the display. For example playback of 23.976fps content is
+ // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the
+ // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions
+ // also lose sync [Internal: b/26453592].
+ return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER);
+ }
+
+ /**
+ * Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
+ * between two {@link Format}s.
+ *
+ * @param first The first format.
+ * @param second The second format.
+ * @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
+ * between two {@link Format}s.
+ */
+ private static boolean areAdaptationCompatible(Format first, Format second) {
+ return first.sampleMimeType.equals(second.sampleMimeType)
+ && getRotationDegrees(first) == getRotationDegrees(second);
+ }
+
+ private static float getPixelWidthHeightRatio(Format format) {
+ return format.pixelWidthHeightRatio == Format.NO_VALUE ? 1 : format.pixelWidthHeightRatio;
+ }
+
+ private static int getRotationDegrees(Format format) {
+ return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees;
+ }
+
+ private static final class CodecMaxValues {
+
+ public final int width;
+ public final int height;
+ public final int inputSize;
+
+ public CodecMaxValues(int width, int height, int inputSize) {
+ this.width = width;
+ this.height = height;
+ this.inputSize = inputSize;
+ }
+
+ }
+
+ @TargetApi(23)
+ private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener {
+
+ private OnFrameRenderedListenerV23(MediaCodec codec) {
+ codec.setOnFrameRenderedListener(this, new Handler());
+ }
+
+ @Override
+ public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) {
+ if (this != tunnelingOnFrameRenderedListener) {
+ // Stale event.
+ return;
+ }
+ maybeNotifyRenderedFirstFrame();
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
@@ -0,0 +1,303 @@
+/*
+ * 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 com.google.android.exoplayer2.video;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.WindowManager;
+import com.google.android.exoplayer2.C;
+
+/**
+ * Makes a best effort to adjust frame release timestamps for a smoother visual result.
+ */
+@TargetApi(16)
+public final class VideoFrameReleaseTimeHelper {
+
+ private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+ private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+ private static final long VSYNC_OFFSET_PERCENTAGE = 80;
+ private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
+
+ private final VSyncSampler vsyncSampler;
+ private final boolean useDefaultDisplayVsync;
+ private final long vsyncDurationNs;
+ private final long vsyncOffsetNs;
+
+ private long lastFramePresentationTimeUs;
+ private long adjustedLastFrameTimeNs;
+ private long pendingAdjustedFrameTimeNs;
+
+ private boolean haveSync;
+ private long syncUnadjustedReleaseTimeNs;
+ private long syncFramePresentationTimeNs;
+ private long frameCount;
+
+ /**
+ * Constructs an instance that smoothes frame release timestamps but does not align them with
+ * the default display's vsync signal.
+ */
+ public VideoFrameReleaseTimeHelper() {
+ this(-1 /* Value unused */, false);
+ }
+
+ /**
+ * Constructs an instance that smoothes frame release timestamps and aligns them with the default
+ * display's vsync signal.
+ *
+ * @param context A context from which information about the default display can be retrieved.
+ */
+ public VideoFrameReleaseTimeHelper(Context context) {
+ this(getDefaultDisplayRefreshRate(context), true);
+ }
+
+ private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate,
+ boolean useDefaultDisplayVsync) {
+ this.useDefaultDisplayVsync = useDefaultDisplayVsync;
+ if (useDefaultDisplayVsync) {
+ vsyncSampler = VSyncSampler.getInstance();
+ vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate);
+ vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+ } else {
+ vsyncSampler = null;
+ vsyncDurationNs = -1; // Value unused.
+ vsyncOffsetNs = -1; // Value unused.
+ }
+ }
+
+ /**
+ * Enables the helper.
+ */
+ public void enable() {
+ haveSync = false;
+ if (useDefaultDisplayVsync) {
+ vsyncSampler.addObserver();
+ }
+ }
+
+ /**
+ * Disables the helper.
+ */
+ public void disable() {
+ if (useDefaultDisplayVsync) {
+ vsyncSampler.removeObserver();
+ }
+ }
+
+ /**
+ * Adjusts a frame release timestamp.
+ *
+ * @param framePresentationTimeUs The frame's presentation time, in microseconds.
+ * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in
+ * the same time base as {@link System#nanoTime()}.
+ * @return The adjusted frame release timestamp, in nanoseconds and in the same time base as
+ * {@link System#nanoTime()}.
+ */
+ public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) {
+ long framePresentationTimeNs = framePresentationTimeUs * 1000;
+
+ // Until we know better, the adjustment will be a no-op.
+ long adjustedFrameTimeNs = framePresentationTimeNs;
+ long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+ if (haveSync) {
+ // See if we've advanced to the next frame.
+ if (framePresentationTimeUs != lastFramePresentationTimeUs) {
+ frameCount++;
+ adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+ }
+ if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+ // We're synced and have waited the required number of frames to apply an adjustment.
+ // Calculate the average frame time across all the frames we've seen since the last sync.
+ // This will typically give us a frame rate at a finer granularity than the frame times
+ // themselves (which often only have millisecond granularity).
+ long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs)
+ / frameCount;
+ // Project the adjusted frame time forward using the average.
+ long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs;
+
+ if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ } else {
+ adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+ adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs
+ - syncFramePresentationTimeNs;
+ }
+ } else {
+ // We're synced but haven't waited the required number of frames to apply an adjustment.
+ // Check drift anyway.
+ if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) {
+ haveSync = false;
+ }
+ }
+ }
+
+ // If we need to sync, do so now.
+ if (!haveSync) {
+ syncFramePresentationTimeNs = framePresentationTimeNs;
+ syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+ frameCount = 0;
+ haveSync = true;
+ onSynced();
+ }
+
+ lastFramePresentationTimeUs = framePresentationTimeUs;
+ pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+ if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
+ return adjustedReleaseTimeNs;
+ }
+
+ // Find the timestamp of the closest vsync. This is the vsync that we're targeting.
+ long snappedTimeNs = closestVsync(adjustedReleaseTimeNs,
+ vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
+ // Apply an offset so that we release before the target vsync, but after the previous one.
+ return snappedTimeNs - vsyncOffsetNs;
+ }
+
+ protected void onSynced() {
+ // Do nothing.
+ }
+
+ private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
+ long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs;
+ long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs;
+ return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
+ }
+
+ private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
+ long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+ long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+ long snappedBeforeNs;
+ long snappedAfterNs;
+ if (releaseTime <= snappedTimeNs) {
+ snappedBeforeNs = snappedTimeNs - vsyncDuration;
+ snappedAfterNs = snappedTimeNs;
+ } else {
+ snappedBeforeNs = snappedTimeNs;
+ snappedAfterNs = snappedTimeNs + vsyncDuration;
+ }
+ long snappedAfterDiff = snappedAfterNs - releaseTime;
+ long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+ return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
+ }
+
+ private static float getDefaultDisplayRefreshRate(Context context) {
+ WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ return manager.getDefaultDisplay().getRefreshRate();
+ }
+
+ /**
+ * Samples display vsync timestamps. A single instance using a single {@link Choreographer} is
+ * shared by all {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a resource
+ * leak in the platform on API levels prior to 23. See [Internal: b/12455729].
+ */
+ private static final class VSyncSampler implements FrameCallback, Handler.Callback {
+
+ public volatile long sampledVsyncTimeNs;
+
+ private static final int CREATE_CHOREOGRAPHER = 0;
+ private static final int MSG_ADD_OBSERVER = 1;
+ private static final int MSG_REMOVE_OBSERVER = 2;
+
+ private static final VSyncSampler INSTANCE = new VSyncSampler();
+
+ private final Handler handler;
+ private final HandlerThread choreographerOwnerThread;
+ private Choreographer choreographer;
+ private int observerCount;
+
+ public static VSyncSampler getInstance() {
+ return INSTANCE;
+ }
+
+ private VSyncSampler() {
+ choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler");
+ choreographerOwnerThread.start();
+ handler = new Handler(choreographerOwnerThread.getLooper(), this);
+ handler.sendEmptyMessage(CREATE_CHOREOGRAPHER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is observing
+ * {@link #sampledVsyncTimeNs}, and hence that the value should be periodically updated.
+ */
+ public void addObserver() {
+ handler.sendEmptyMessage(MSG_ADD_OBSERVER);
+ }
+
+ /**
+ * Notifies the sampler that a {@link VideoFrameReleaseTimeHelper} is no longer observing
+ * {@link #sampledVsyncTimeNs}.
+ */
+ public void removeObserver() {
+ handler.sendEmptyMessage(MSG_REMOVE_OBSERVER);
+ }
+
+ @Override
+ public void doFrame(long vsyncTimeNs) {
+ sampledVsyncTimeNs = vsyncTimeNs;
+ choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+ }
+
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case CREATE_CHOREOGRAPHER: {
+ createChoreographerInstanceInternal();
+ return true;
+ }
+ case MSG_ADD_OBSERVER: {
+ addObserverInternal();
+ return true;
+ }
+ case MSG_REMOVE_OBSERVER: {
+ removeObserverInternal();
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ private void createChoreographerInstanceInternal() {
+ choreographer = Choreographer.getInstance();
+ }
+
+ private void addObserverInternal() {
+ observerCount++;
+ if (observerCount == 1) {
+ choreographer.postFrameCallback(this);
+ }
+ }
+
+ private void removeObserverInternal() {
+ observerCount--;
+ if (observerCount == 0) {
+ choreographer.removeFrameCallback(this);
+ sampledVsyncTimeNs = 0;
+ }
+ }
+
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java
@@ -0,0 +1,230 @@
+/*
+ * 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 com.google.android.exoplayer2.video;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.Surface;
+import android.view.TextureView;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Listener of video {@link Renderer} events.
+ */
+public interface VideoRendererEventListener {
+
+ /**
+ * Called when the renderer is enabled.
+ *
+ * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
+ * remains enabled.
+ */
+ void onVideoEnabled(DecoderCounters counters);
+
+ /**
+ * Called when a decoder is created.
+ *
+ * @param decoderName The decoder that was created.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
+ */
+ void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
+ long initializationDurationMs);
+
+ /**
+ * Called when the format of the media being consumed by the renderer changes.
+ *
+ * @param format The new format.
+ */
+ void onVideoInputFormatChanged(Format format);
+
+ /**
+ * Called to report the number of frames dropped by the renderer. Dropped frames are reported
+ * whenever the renderer is stopped having dropped frames, and optionally, whenever the count
+ * reaches a specified threshold whilst the renderer is started.
+ *
+ * @param count The number of dropped frames.
+ * @param elapsedMs The duration in milliseconds over which the frames were dropped. This
+ * duration is timed from when the renderer was started or from when dropped frames were
+ * last reported (whichever was more recent), and not from when the first of the reported
+ * drops occurred.
+ */
+ void onDroppedFrames(int count, long elapsedMs);
+
+ /**
+ * Called before a frame is rendered for the first time since setting the surface, and each time
+ * there's a change in the size, rotation or pixel aspect ratio of the video being rendered.
+ *
+ * @param width The video width in pixels.
+ * @param height The video height in pixels.
+ * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
+ * rotation in degrees that the application should apply for the video for it to be rendered
+ * in the correct orientation. This value will always be zero on API levels 21 and above,
+ * since the renderer will apply all necessary rotations internally. On earlier API levels
+ * this is not possible. Applications that use {@link TextureView} can apply the rotation by
+ * calling {@link TextureView#setTransform}. Applications that do not expect to encounter
+ * rotated videos can safely ignore this parameter.
+ * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
+ * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
+ * content.
+ */
+ void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+ float pixelWidthHeightRatio);
+
+ /**
+ * Called when a frame is rendered for the first time since setting the surface, and when a frame
+ * is rendered for the first time since the renderer was reset.
+ *
+ * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
+ * the renderer renders to something that isn't a {@link Surface}.
+ */
+ void onRenderedFirstFrame(Surface surface);
+
+ /**
+ * Called when the renderer is disabled.
+ *
+ * @param counters {@link DecoderCounters} that were updated by the renderer.
+ */
+ void onVideoDisabled(DecoderCounters counters);
+
+ /**
+ * Dispatches events to a {@link VideoRendererEventListener}.
+ */
+ final class EventDispatcher {
+
+ private final Handler handler;
+ private final VideoRendererEventListener listener;
+
+ /**
+ * @param handler A handler for dispatching events, or null if creating a dummy instance.
+ * @param listener The listener to which events should be dispatched, or null if creating a
+ * dummy instance.
+ */
+ public EventDispatcher(Handler handler, VideoRendererEventListener listener) {
+ this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+ this.listener = listener;
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}.
+ */
+ public void enabled(final DecoderCounters decoderCounters) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onVideoEnabled(decoderCounters);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}.
+ */
+ public void decoderInitialized(final String decoderName,
+ final long initializedTimestampMs, final long initializationDurationMs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
+ initializationDurationMs);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}.
+ */
+ public void inputFormatChanged(final Format format) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onVideoInputFormatChanged(format);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+ */
+ public void droppedFrames(final int droppedFrameCount, final long elapsedMs) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onDroppedFrames(droppedFrameCount, elapsedMs);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}.
+ */
+ public void videoSizeChanged(final int width, final int height,
+ final int unappliedRotationDegrees, final float pixelWidthHeightRatio) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
+ pixelWidthHeightRatio);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}.
+ */
+ public void renderedFirstFrame(final Surface surface) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onRenderedFirstFrame(surface);
+ }
+ });
+ }
+ }
+
+ /**
+ * Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}.
+ */
+ public void disabled(final DecoderCounters counters) {
+ if (listener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ counters.ensureUpdated();
+ listener.onVideoDisabled(counters);
+ }
+ });
+ }
+ }
+
+ }
+
+}