out) {
- out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder()));
+ out.add(new MetadataRenderer(output, mainHandler.getLooper()));
}
/**
diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
index 072180db94..b5873904fc 100644
--- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
+++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -38,21 +38,21 @@ import java.nio.ByteOrder;
* playback position smoothing, non-blocking writes and reconfiguration.
*
* Before starting playback, specify the input format by calling
- * {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)} or
- * {@link #initializeV21(int, boolean)}, optionally specifying an audio session and whether the
- * track is to be used with tunneling video playback.
+ * {@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.
*
* 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.
*
- * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If
- * {@link #isInitialized()} returns {@code false} after the call, it is necessary to call
- * {@link #initialize(int)} or {@link #initializeV21(int, boolean)} before writing more data.
+ * 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)}.
*
- * The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and
- * released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input
- * format is unchanged). It is safe to call {@link #initialize(int)} or
- * {@link #initializeV21(int, boolean)} after calling {@link #reset()} without reconfiguration.
+ * 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)}.
*
* Call {@link #release()} when the instance is no longer required.
*/
@@ -63,6 +63,19 @@ public final class AudioTrack {
*/
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.
*
@@ -137,15 +150,6 @@ public final class AudioTrack {
}
- /**
- * Returned in the result of {@link #handleBuffer} if the buffer was discontinuous.
- */
- public static final int RESULT_POSITION_DISCONTINUITY = 1;
- /**
- * Returned in the result of {@link #handleBuffer} if the buffer can be released.
- */
- public static final int RESULT_BUFFER_CONSUMED = 2;
-
/**
* Returned by {@link #getCurrentPositionUs} when the position is not set.
*/
@@ -253,7 +257,7 @@ public final class AudioTrack {
private final AudioTrackUtil audioTrackUtil;
/**
- * Used to keep the audio session active on pre-V21 builds (see {@link #initialize(int)}).
+ * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
*/
private android.media.AudioTrack keepSessionIdAudioTrack;
@@ -271,7 +275,6 @@ public final class AudioTrack {
private int bufferSize;
private long bufferSizeUs;
- private boolean useHwAvSync;
private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync;
@@ -299,6 +302,9 @@ public final class AudioTrack {
private ByteBuffer resampledBuffer;
private boolean useResampledBuffer;
+ private boolean playing;
+ private int audioSessionId;
+ private boolean tunneling;
private boolean hasData;
private long lastFeedElapsedRealtimeMs;
@@ -329,6 +335,7 @@ public final class AudioTrack {
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
streamType = C.STREAM_TYPE_DEFAULT;
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
/**
@@ -342,14 +349,6 @@ public final class AudioTrack {
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
}
- /**
- * Returns whether the audio track has been successfully initialized via {@link #initialize} or
- * {@link #initializeV21(int, boolean)}, and has not yet been {@link #reset}.
- */
- public boolean isInitialized() {
- return audioTrack != null;
- }
-
/**
* Returns the playback position in the stream starting at zero, in microseconds, or
* {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
@@ -446,7 +445,7 @@ public final class AudioTrack {
// 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) {
+ switch (channelCount) {
case 7:
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
break;
@@ -460,6 +459,13 @@ public final class AudioTrack {
}
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
+
+ // 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;
+ }
+
@C.Encoding int sourceEncoding;
if (passthrough) {
sourceEncoding = getEncodingForMimeType(mimeType);
@@ -512,31 +518,7 @@ public final class AudioTrack {
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
}
- /**
- * Initializes the audio track for writing new buffers using {@link #handleBuffer}.
- *
- * @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create
- * one.
- * @return The audio track session identifier.
- */
- public int initialize(int sessionId) throws InitializationException {
- return initializeInternal(sessionId, false);
- }
-
- /**
- * Initializes the audio track for writing new buffers using {@link #handleBuffer}.
- *
- * @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create
- * one.
- * @param tunneling Whether the audio track is to be used with tunneling video playback.
- * @return The audio track session identifier.
- */
- public int initializeV21(int sessionId, boolean tunneling) throws InitializationException {
- Assertions.checkState(Util.SDK_INT >= 21);
- return initializeInternal(sessionId, tunneling);
- }
-
- private int initializeInternal(int sessionId, boolean tunneling) throws InitializationException {
+ 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
@@ -544,27 +526,26 @@ public final class AudioTrack {
// initialization of the audio track to fail.
releasingConditionVariable.block();
- useHwAvSync = tunneling;
- if (useHwAvSync) {
+ if (tunneling) {
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
- bufferSize, sessionId);
- } else if (sessionId == C.AUDIO_SESSION_ID_UNSET) {
+ bufferSize, audioSessionId);
+ } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM);
} else {
// Re-attach to the same audio session.
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
- targetEncoding, bufferSize, MODE_STREAM, sessionId);
+ targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
}
checkAudioTrackInitialized();
- sessionId = audioTrack.getAudioSessionId();
+ 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
- && sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
+ && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
releaseKeepSessionIdAudioTrack();
}
if (keepSessionIdAudioTrack == null) {
@@ -573,21 +554,25 @@ public final class AudioTrack {
@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, sessionId);
+ channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
}
}
}
+ if (this.audioSessionId != audioSessionId) {
+ this.audioSessionId = audioSessionId;
+ listener.onAudioSessionId(audioSessionId);
+ }
audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
- setAudioTrackVolume();
+ setVolumeInternal();
hasData = false;
- return sessionId;
}
/**
* 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();
@@ -608,35 +593,41 @@ public final class AudioTrack {
* Attempts to write data from a {@link ByteBuffer} to the audio track, 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 successfully written.
+ * {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
+ * discontinuous with the last buffer handled since the track was reset.
*
- * Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full,
- * and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously
- * written data.
- *
- * If the data was not written 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 #configure} or {@link #reset}.
+ * Returns whether the data was written in full. If the data was not written 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 to play back.
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
- * @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and
- * {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously
- * written data.
+ * @return Whether the buffer was consumed fully.
+ * @throws InitializationException If an error occurs initializing the track.
* @throws WriteException If an error occurs writing the audio data.
*/
- public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
+ public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
+ throws InitializationException, WriteException {
+ if (!isInitialized()) {
+ initialize();
+ if (playing) {
+ play();
+ }
+ }
+
boolean hadData = hasData;
hasData = hasPendingData();
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
}
- int result = writeBuffer(buffer, presentationTimeUs);
+ boolean result = writeBuffer(buffer, presentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
return result;
}
- private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
+ private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
boolean isNewSourceBuffer = currentSourceBuffer == null;
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
currentSourceBuffer = buffer;
@@ -645,7 +636,7 @@ public final class AudioTrack {
// 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) {
- return 0;
+ return false;
}
// A new AC-3 audio track's playback position continues to increase from the old track's
@@ -653,18 +644,17 @@ public final class AudioTrack {
// head position actually returns to zero.
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
- return 0;
+ return false;
}
}
- int result = 0;
if (isNewSourceBuffer) {
// We're seeing this buffer for the first time.
if (!currentSourceBuffer.hasRemaining()) {
// The buffer is empty.
currentSourceBuffer = null;
- return RESULT_BUFFER_CONSUMED;
+ return true;
}
useResampledBuffer = targetEncoding != sourceEncoding;
@@ -697,7 +687,7 @@ public final class AudioTrack {
// number of bytes submitted.
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
startMediaTimeState = START_IN_SYNC;
- result |= RESULT_POSITION_DISCONTINUITY;
+ listener.onPositionDiscontinuity();
}
}
if (Util.SDK_INT < 21) {
@@ -730,7 +720,7 @@ public final class AudioTrack {
buffer.position(buffer.position() + bytesWritten);
}
} else {
- bytesWritten = useHwAvSync
+ bytesWritten = tunneling
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
}
@@ -747,9 +737,9 @@ public final class AudioTrack {
submittedEncodedFrames += framesPerEncodedSample;
}
currentSourceBuffer = null;
- result |= RESULT_BUFFER_CONSUMED;
+ return true;
}
- return result;
+ return false;
}
/**
@@ -785,28 +775,64 @@ public final class AudioTrack {
/**
* Sets the stream type for audio track. If the stream type has changed and if the audio track
- * is not configured for use with video tunneling, then the audio track is reset and the caller
- * must re-initialize the audio track before writing more data. The caller must not reuse the
- * audio session identifier when re-initializing with a new stream type.
+ * is not configured for use with tunneling, then the audio track is reset and the audio session
+ * id is cleared.
*
- * If the audio track is configured for use with video tunneling then the stream type is ignored
- * and the audio track is not reset. The passed stream type will be used if the audio track is
- * later re-configured into non-tunneled mode.
+ * 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.
- * @return Whether the audio track was reset as a result of this call.
*/
- public boolean setStreamType(@C.StreamType int streamType) {
+ public void setStreamType(@C.StreamType int streamType) {
if (this.streamType == streamType) {
- return false;
+ return;
}
this.streamType = streamType;
- if (useHwAvSync) {
+ if (tunneling) {
// The stream type is ignored in tunneling mode, so no need to reset.
- return false;
+ return;
}
reset();
- return true;
+ 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.
+ *
+ * @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();
+ }
}
/**
@@ -817,17 +843,17 @@ public final class AudioTrack {
public void setVolume(float volume) {
if (this.volume != volume) {
this.volume = volume;
- setAudioTrackVolume();
+ setVolumeInternal();
}
}
- private void setAudioTrackVolume() {
+ private void setVolumeInternal() {
if (!isInitialized()) {
// Do nothing.
} else if (Util.SDK_INT >= 21) {
- setAudioTrackVolumeV21(audioTrack, volume);
+ setVolumeInternalV21(audioTrack, volume);
} else {
- setAudioTrackVolumeV3(audioTrack, volume);
+ setVolumeInternalV3(audioTrack, volume);
}
}
@@ -835,6 +861,7 @@ public final class AudioTrack {
* Pauses playback.
*/
public void pause() {
+ playing = false;
if (isInitialized()) {
resetSyncParams();
audioTrackUtil.pause();
@@ -844,9 +871,9 @@ public final class AudioTrack {
/**
* Releases the underlying audio track asynchronously.
*
- * Calling {@link #initialize(int)} or {@link #initializeV21(int, boolean)} will block until the
- * audio track has been released, so it is safe to initialize immediately after a reset. The audio
- * session may remain active until {@link #release()} is called.
+ * 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()) {
@@ -855,6 +882,7 @@ public final class AudioTrack {
framesPerEncodedSample = 0;
currentSourceBuffer = null;
avSyncHeader = null;
+ bytesUntilNextAvSync = 0;
startMediaTimeState = START_NOT_SET;
latencyUs = 0;
resetSyncParams();
@@ -887,6 +915,8 @@ public final class AudioTrack {
public void release() {
reset();
releaseKeepSessionIdAudioTrack();
+ audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ playing = false;
}
/**
@@ -1024,6 +1054,10 @@ public final class AudioTrack {
throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
}
+ private boolean isInitialized() {
+ return audioTrack != null;
+ }
+
private long pcmBytesToFrames(long byteCount) {
return byteCount / pcmFrameSize;
}
@@ -1240,12 +1274,12 @@ public final class AudioTrack {
}
@TargetApi(21)
- private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) {
+ private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) {
audioTrack.setVolume(volume);
}
@SuppressWarnings("deprecation")
- private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) {
+ private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) {
audioTrack.setStereoVolume(volume, volume);
}
@@ -1494,7 +1528,7 @@ public final class AudioTrack {
playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
.allowDefaults();
this.playbackParams = playbackParams;
- this.playbackSpeed = playbackParams.getSpeed();
+ playbackSpeed = playbackParams.getSpeed();
maybeApplyPlaybackParams();
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index d3cde10afb..b4813d90a2 100644
--- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -41,8 +41,7 @@ import java.nio.ByteBuffer;
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
*/
@TargetApi(16)
-public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock,
- AudioTrack.Listener {
+public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack;
@@ -50,7 +49,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean passthroughEnabled;
private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding;
- private int audioSessionId;
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
@@ -129,8 +127,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
- audioSessionId = C.AUDIO_SESSION_ID_UNSET;
- audioTrack = new AudioTrack(audioCapabilities, this);
+ audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
}
@@ -141,10 +138,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
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 | FORMAT_HANDLED;
+ return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
}
- MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false);
+ MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE;
}
@@ -155,7 +153,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& (format.channelCount == Format.NO_VALUE
|| decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
- return ADAPTIVE_NOT_SEAMLESS | formatSupport;
+ return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
}
@Override
@@ -231,25 +229,42 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
/**
- * Called when the audio session id becomes known. Once the id is known it will not change (and
- * hence this method will not be called again) unless the renderer is disabled and then
- * subsequently re-enabled.
- *
- * 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).
+ * 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).
*
- * @param audioSessionId The audio session id.
+ * @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
@@ -274,7 +289,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onDisabled() {
- audioSessionId = C.AUDIO_SESSION_ID_UNSET;
try {
audioTrack.release();
} finally {
@@ -325,44 +339,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return true;
}
- if (!audioTrack.isInitialized()) {
- // Initialize the AudioTrack now.
- try {
- if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
- audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET);
- eventDispatcher.audioSessionId(audioSessionId);
- onAudioSessionId(audioSessionId);
- } else {
- audioTrack.initialize(audioSessionId);
- }
- } catch (AudioTrack.InitializationException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
- }
- if (getState() == STATE_STARTED) {
- audioTrack.play();
- }
- }
-
- int handleBufferResult;
try {
- handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
- } catch (AudioTrack.WriteException e) {
+ if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
+ codec.releaseOutputBuffer(bufferIndex, false);
+ decoderCounters.renderedOutputBufferCount++;
+ return true;
+ }
+ } catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
-
- // If we are out of sync, allow currentPositionUs to jump backwards.
- if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
- handleAudioTrackDiscontinuity();
- allowPositionDiscontinuity = true;
- }
-
- // Release the buffer if it was consumed.
- if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
- codec.releaseOutputBuffer(bufferIndex, false);
- decoderCounters.renderedOutputBufferCount++;
- return true;
- }
-
return false;
}
@@ -371,10 +356,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioTrack.handleEndOfStream();
}
- protected void handleAudioTrackDiscontinuity() {
- // Do nothing
- }
-
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
@@ -386,9 +367,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
- if (audioTrack.setStreamType(streamType)) {
- audioSessionId = C.AUDIO_SESSION_ID_UNSET;
- }
+ audioTrack.setStreamType(streamType);
break;
default:
super.handleMessage(messageType, message);
@@ -396,11 +375,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
- // AudioTrack.Listener implementation.
+ 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);
+ }
- @Override
- public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
- eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index 5c9acc7739..d23ee769dd 100644
--- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio;
import android.media.PlaybackParams;
+import android.media.audiofx.Virtualizer;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
@@ -43,8 +44,7 @@ import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders audio using a {@link SimpleDecoder}.
*/
-public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock,
- AudioTrack.Listener {
+public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
@Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
@@ -94,8 +94,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private boolean outputStreamEnded;
private boolean waitingForKeys;
- private int audioSessionId;
-
public SimpleDecoderAudioRenderer() {
this(null, null);
}
@@ -141,11 +139,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
- audioTrack = new AudioTrack(audioCapabilities, this);
+ audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
- audioSessionId = C.AUDIO_SESSION_ID_UNSET;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true;
}
@@ -155,6 +152,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
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) {
@@ -185,6 +201,33 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
}
+ /**
+ * 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.
*
@@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = false;
}
- if (!audioTrack.isInitialized()) {
- if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
- audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET);
- eventDispatcher.audioSessionId(audioSessionId);
- onAudioSessionId(audioSessionId);
- } else {
- audioTrack.initialize(audioSessionId);
- }
- if (getState() == STATE_STARTED) {
- audioTrack.play();
- }
- }
-
- int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs);
-
- // If we are out of sync, allow currentPositionUs to jump backwards.
- if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
- allowPositionDiscontinuity = true;
- }
-
- // Release the buffer if it was consumed.
- if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
+ if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
decoderCounters.renderedOutputBufferCount++;
outputBuffer.release();
outputBuffer = null;
@@ -381,23 +403,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return currentPositionUs;
}
- /**
- * Called when the audio session id becomes known. Once the id is known it will not change (and
- * hence this method will not be called again) unless the renderer is disabled and then
- * subsequently re-enabled.
- *
- * The default implementation is a no-op.
- *
- * @param audioSessionId The audio session id.
- */
- protected void onAudioSessionId(int audioSessionId) {
- // Do nothing.
- }
-
@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
@@ -425,7 +440,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
protected void onDisabled() {
inputFormat = null;
- audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioTrackNeedsConfigure = true;
waitingForKeys = false;
try {
@@ -537,6 +551,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
// There aren't any final output buffers, so release the decoder immediately.
releaseDecoder();
maybeInitDecoder();
+ audioTrackNeedsConfigure = true;
}
eventDispatcher.inputFormatChanged(newFormat);
@@ -553,9 +568,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
- if (audioTrack.setStreamType(streamType)) {
- audioSessionId = C.AUDIO_SESSION_ID_UNSET;
- }
+ audioTrack.setStreamType(streamType);
break;
default:
super.handleMessage(messageType, message);
@@ -563,11 +576,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
}
- // AudioTrack.Listener implementation.
+ 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);
+ }
- @Override
- public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
- eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
similarity index 63%
rename from library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java
rename to library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index 4e4845c70b..9c959a38c5 100644
--- a/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java
+++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -24,7 +24,10 @@ 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;
@@ -33,18 +36,21 @@ 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.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 streaming playbacks using {@link MediaDrm}.
+ * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
*/
@TargetApi(18)
-public class StreamingDrmSessionManager implements DrmSessionManager,
+public class DefaultDrmSessionManager implements DrmSessionManager,
DrmSession {
/**
- * Listener of {@link StreamingDrmSessionManager} events.
+ * Listener of {@link DefaultDrmSessionManager} events.
*/
public interface EventListener {
@@ -60,6 +66,16 @@ public class StreamingDrmSessionManager implements Drm
*/
void onDrmSessionManagerError(Exception e);
+ /**
+ * Called each time offline keys are restored.
+ */
+ void onDrmKeysRestored();
+
+ /**
+ * Called each time offline keys are removed.
+ */
+ void onDrmKeysRemoved();
+
}
/**
@@ -67,9 +83,32 @@ public class StreamingDrmSessionManager implements Drm
*/
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 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 mediaDrm;
@@ -85,14 +124,17 @@ public class StreamingDrmSessionManager implements Drm
private HandlerThread requestHandlerThread;
private Handler postRequestHandler;
+ private int mode;
private int openCount;
private boolean provisioningInProgress;
@DrmSession.State
private int state;
private T mediaCrypto;
- private Exception lastException;
- private SchemeData schemeData;
+ private DrmSessionException lastException;
+ private byte[] schemeInitData;
+ private String schemeMimeType;
private byte[] sessionId;
+ private byte[] offlineLicenseKeySetId;
/**
* Instantiates a new instance using the Widevine scheme.
@@ -105,7 +147,7 @@ public class StreamingDrmSessionManager implements Drm
* @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 StreamingDrmSessionManager newWidevineInstance(
+ public static DefaultDrmSessionManager newWidevineInstance(
MediaDrmCallback callback, HashMap optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
@@ -125,7 +167,7 @@ public class StreamingDrmSessionManager implements Drm
* @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 StreamingDrmSessionManager newPlayReadyInstance(
+ public static DefaultDrmSessionManager newPlayReadyInstance(
MediaDrmCallback callback, String customData, Handler eventHandler,
EventListener eventListener) throws UnsupportedDrmException {
HashMap optionalKeyRequestParameters;
@@ -151,10 +193,10 @@ public class StreamingDrmSessionManager implements Drm
* @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 StreamingDrmSessionManager newFrameworkInstance(
+ public static DefaultDrmSessionManager newFrameworkInstance(
UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
- return new StreamingDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
+ return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
optionalKeyRequestParameters, eventHandler, eventListener);
}
@@ -168,7 +210,7 @@ public class StreamingDrmSessionManager implements Drm
* 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 StreamingDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
+ public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback,
HashMap optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener) {
this.uuid = uuid;
@@ -179,6 +221,7 @@ public class StreamingDrmSessionManager implements Drm
this.eventListener = eventListener;
mediaDrm.setOnEventListener(new MediaDrmEventListener());
state = STATE_CLOSED;
+ mode = MODE_PLAYBACK;
}
/**
@@ -229,6 +272,35 @@ public class StreamingDrmSessionManager implements Drm
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.
+ *
+ * By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
+ * required.
+ *
+ *
{@code mode} must be one of these:
+ *
{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
+ * requested otherwise the offline license is restored.
+ * {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
+ * is restored.
+ * {@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
+ * requested otherwise the offline license is renewed.
+ * {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
+ * is released.
+ *
+ * @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
@@ -248,18 +320,22 @@ public class StreamingDrmSessionManager implements Drm
requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
- schemeData = drmInitData.get(uuid);
- if (schemeData == null) {
- onError(new IllegalStateException("Media does not support uuid: " + uuid));
- return this;
- }
- if (Util.SDK_INT < 21) {
- // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
- byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID);
- if (psshData == null) {
- // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
- } else {
- schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData);
+ 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;
+ }
}
}
state = STATE_OPENING;
@@ -280,7 +356,8 @@ public class StreamingDrmSessionManager implements Drm
postRequestHandler = null;
requestHandlerThread.quit();
requestHandlerThread = null;
- schemeData = null;
+ schemeInitData = null;
+ schemeMimeType = null;
mediaCrypto = null;
lastException = null;
if (sessionId != null) {
@@ -314,10 +391,25 @@ public class StreamingDrmSessionManager implements Drm
}
@Override
- public final Exception getError() {
+ public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null;
}
+ @Override
+ public Map 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) {
@@ -325,7 +417,7 @@ public class StreamingDrmSessionManager implements Drm
sessionId = mediaDrm.openSession();
mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
state = STATE_OPENED;
- postKeyRequest();
+ doLicense();
} catch (NotProvisionedException e) {
if (allowProvisioning) {
postProvisionRequest();
@@ -363,20 +455,87 @@ public class StreamingDrmSessionManager implements Drm
if (state == STATE_OPENING) {
openInternal(false);
} else {
- postKeyRequest();
+ doLicense();
}
} catch (DeniedByServerException e) {
onError(e);
}
}
- private void postKeyRequest() {
+ 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 pair = WidevineUtil.getLicenseDurationRemainingSec(this);
+ return Math.min(pair.first, pair.second);
+ }
+
+ private void postKeyRequest(byte[] scope, int keyType) {
KeyRequest keyRequest;
try {
- keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType,
- MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters);
+ keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
+ optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
- } catch (NotProvisionedException e) {
+ } catch (Exception e) {
onKeysError(e);
}
}
@@ -393,15 +552,30 @@ public class StreamingDrmSessionManager implements Drm
}
try {
- mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
- state = STATE_OPENED_WITH_KEYS;
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmKeysLoaded();
- }
- });
+ 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 (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);
@@ -417,7 +591,7 @@ public class StreamingDrmSessionManager implements Drm
}
private void onError(final Exception e) {
- lastException = e;
+ lastException = new DrmSessionException(e);
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
@@ -446,11 +620,16 @@ public class StreamingDrmSessionManager implements Drm
}
switch (msg.what) {
case MediaDrm.EVENT_KEY_REQUIRED:
- postKeyRequest();
+ doLicense();
break;
case MediaDrm.EVENT_KEY_EXPIRED:
- state = STATE_OPENED;
- onError(new KeysExpiredException());
+ // 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;
@@ -466,7 +645,9 @@ public class StreamingDrmSessionManager implements Drm
@Override
public void onEvent(ExoMediaDrm extends T> md, byte[] sessionId, int event, int extra,
byte[] data) {
- mediaDrmHandler.sendEmptyMessage(event);
+ if (mode == MODE_PLAYBACK) {
+ mediaDrmHandler.sendEmptyMessage(event);
+ }
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
index 6f84395072..4d64187a8b 100644
--- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
+++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java
@@ -16,9 +16,11 @@
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.
@@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
@TargetApi(16)
public interface DrmSession {
+ /** Wraps the exception which is the cause of the error state. */
+ class DrmSessionException extends Exception {
+
+ DrmSessionException(Exception e) {
+ super(e);
+ }
+
+ }
+
/**
* The state of the DRM session.
*/
@@ -96,6 +107,26 @@ public interface DrmSession {
*
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
*/
- Exception getError();
+ DrmSessionException getError();
+
+ /**
+ * Returns an informative description of the key status for the session. The status is in the form
+ * of {name, value} pairs.
+ *
+ * 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 queryKeyStatus();
+
+ /**
+ * Returns the key set id of the offline license loaded into this session, if there is one. Null
+ * otherwise.
+ */
+ byte[] getOfflineLicenseKeySetId();
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
index 65e41dd91e..e0c9ca5296 100644
--- a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
+++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
@@ -105,7 +105,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
try {
return Util.toByteArray(inputStream);
} finally {
- inputStream.close();
+ Util.closeQuietly(inputStream);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
new file mode 100644
index 0000000000..a11d65d4d3
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.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.drm;
+
+import android.media.MediaDrm;
+import android.net.Uri;
+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.Format;
+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.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
+import com.google.android.exoplayer2.source.chunk.InitializationChunk;
+import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
+import com.google.android.exoplayer2.source.dash.manifest.Period;
+import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
+import com.google.android.exoplayer2.source.dash.manifest.Representation;
+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.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Helper class to download, renew and release offline licenses. It utilizes {@link
+ * DefaultDrmSessionManager}.
+ */
+public final class OfflineLicenseHelper {
+
+ private final ConditionVariable conditionVariable;
+ private final DefaultDrmSessionManager drmSessionManager;
+ private final HandlerThread handlerThread;
+
+ /**
+ * Helper method to download a DASH manifest.
+ *
+ * @param dataSource The {@link HttpDataSource} from which the manifest should be read.
+ * @param manifestUriString The URI of the manifest to be read.
+ * @return An instance of {@link DashManifest}.
+ * @throws IOException If an error occurs reading data from the stream.
+ * @see DashManifestParser
+ */
+ public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString)
+ throws IOException {
+ DataSourceInputStream inputStream = new DataSourceInputStream(
+ dataSource, new DataSpec(Uri.parse(manifestUriString)));
+ try {
+ inputStream.open();
+ DashManifestParser parser = new DashManifestParser();
+ return parser.parse(dataSource.getUri(), inputStream);
+ } finally {
+ inputStream.close();
+ }
+ }
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
+ * you're done with the helper instance.
+ *
+ * @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 newWidevineInstance(
+ String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
+ return newWidevineInstance(
+ new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
+ }
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
+ * you're done with the helper instance.
+ *
+ * @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 newWidevineInstance(
+ MediaDrmCallback callback, HashMap optionalKeyRequestParameters)
+ throws UnsupportedDrmException {
+ return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback,
+ optionalKeyRequestParameters);
+ }
+
+ /**
+ * Constructs an instance. Call {@link #releaseResources()} when you're done with it.
+ *
+ * @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 mediaDrm, MediaDrmCallback callback,
+ HashMap 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 used resources. */
+ public void releaseResources() {
+ handlerThread.quit();
+ }
+
+ /**
+ * Downloads an offline license.
+ *
+ * @param dataSource The {@link HttpDataSource} to be used for download.
+ * @param manifestUriString The URI of the manifest to be read.
+ * @return The downloaded offline license key set id.
+ * @throws IOException If an error occurs reading data from the stream.
+ * @throws InterruptedException If the thread has been interrupted.
+ * @throws DrmSessionException Thrown when there is an error during DRM session.
+ */
+ public byte[] download(HttpDataSource dataSource, String manifestUriString)
+ throws IOException, InterruptedException, DrmSessionException {
+ return download(dataSource, downloadManifest(dataSource, manifestUriString));
+ }
+
+ /**
+ * Downloads an offline license.
+ *
+ * @param dataSource The {@link HttpDataSource} to be used for download.
+ * @param dashManifest The {@link DashManifest} of the DASH content.
+ * @return The downloaded offline license key set id.
+ * @throws IOException If an error occurs reading data from the stream.
+ * @throws InterruptedException If the thread has been interrupted.
+ * @throws DrmSessionException Thrown when there is an error during DRM session.
+ */
+ public byte[] download(HttpDataSource dataSource, DashManifest dashManifest)
+ throws IOException, InterruptedException, DrmSessionException {
+ // Get DrmInitData
+ // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
+ // as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
+ if (dashManifest.getPeriodCount() < 1) {
+ return null;
+ }
+ Period period = dashManifest.getPeriod(0);
+ int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);
+ if (adaptationSetIndex == C.INDEX_UNSET) {
+ adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO);
+ if (adaptationSetIndex == C.INDEX_UNSET) {
+ return null;
+ }
+ }
+ AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
+ if (adaptationSet.representations.isEmpty()) {
+ return null;
+ }
+ Representation representation = adaptationSet.representations.get(0);
+ DrmInitData drmInitData = representation.format.drmInitData;
+ if (drmInitData == null) {
+ InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation);
+ if (initializationChunk == null) {
+ return null;
+ }
+ Format sampleFormat = initializationChunk.getSampleFormat();
+ if (sampleFormat != null) {
+ drmInitData = sampleFormat.drmInitData;
+ }
+ if (drmInitData == null) {
+ return null;
+ }
+ }
+ blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
+ return drmSessionManager.getOfflineLicenseKeySetId();
+ }
+
+ /**
+ * Renews an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be renewed.
+ * @return Renewed offline license key set id.
+ * @throws DrmSessionException Thrown when there is an error during DRM session.
+ */
+ public byte[] renew(byte[] offlineLicenseKeySetId) throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null);
+ return drmSessionManager.getOfflineLicenseKeySetId();
+ }
+
+ /**
+ * Releases an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be released.
+ * @throws DrmSessionException Thrown when there is an error during DRM session.
+ */
+ public void release(byte[] offlineLicenseKeySetId) throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null);
+ }
+
+ /**
+ * Returns license and playback durations remaining in seconds of the given offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license.
+ */
+ public Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ DrmSession session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY,
+ offlineLicenseKeySetId, null);
+ Pair licenseDurationRemainingSec =
+ WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager);
+ drmSessionManager.releaseSession(session);
+ return licenseDurationRemainingSec;
+ }
+
+ private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
+ DrmInitData drmInitData) throws DrmSessionException {
+ DrmSession session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
+ drmInitData);
+ DrmSessionException error = session.getError();
+ if (error != null) {
+ throw error;
+ }
+ drmSessionManager.releaseSession(session);
+ }
+
+ private DrmSession openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
+ DrmInitData drmInitData) {
+ drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
+ conditionVariable.close();
+ DrmSession session = drmSessionManager.acquireSession(handlerThread.getLooper(),
+ drmInitData);
+ // Block current thread until key loading is finished
+ conditionVariable.block();
+ return session;
+ }
+
+ private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
+ final Representation representation) throws IOException, InterruptedException {
+ RangedUri rangedUri = representation.getInitializationUri();
+ if (rangedUri == null) {
+ return null;
+ }
+ DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start,
+ rangedUri.length, representation.getCacheKey());
+ InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
+ representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
+ newWrappedExtractor(representation.format));
+ initializationChunk.load();
+ return initializationChunk;
+ }
+
+ private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
+ final String mimeType = format.containerMimeType;
+ final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM)
+ || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
+ final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();
+ return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */,
+ false /* resendFormatOnInit */);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java
new file mode 100644
index 0000000000..fc80cfb6fb
--- /dev/null
+++ b/library/src/main/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 getLicenseDurationRemainingSec(DrmSession drmSession) {
+ Map keyStatus = drmSession.queryKeyStatus();
+ return new Pair<>(
+ getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
+ getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
+ }
+
+ private static long getDurationRemainingSec(Map 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;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
index 44756a507e..b3bcd97048 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
@@ -226,13 +226,32 @@ public final class DefaultTrackOutput implements TrackOutput {
}
/**
- * Attempts to skip to the keyframe before the specified time, if it's present in the buffer.
+ * 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, and if {@code timeUs} falls
+ * within the currently buffered media.
+ *
+ * This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}.
*
* @param timeUs The seek time.
* @return Whether the skip was successful.
*/
public boolean skipToKeyframeBefore(long timeUs) {
- long nextOffset = infoQueue.skipToKeyframeBefore(timeUs);
+ return skipToKeyframeBefore(timeUs, false);
+ }
+
+ /**
+ * 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;
}
@@ -246,7 +265,8 @@ public final class DefaultTrackOutput implements TrackOutput {
* @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.
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
+ * caller requires that the format of the stream be read even if it's not changing.
* @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.
@@ -732,7 +752,8 @@ public final class DefaultTrackOutput implements TrackOutput {
* 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.
+ * 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 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.
@@ -742,14 +763,14 @@ public final class DefaultTrackOutput implements TrackOutput {
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
if (queueSize == 0) {
- if (upstreamFormat != null && upstreamFormat != downstreamFormat) {
+ if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
formatHolder.format = upstreamFormat;
return C.RESULT_FORMAT_READ;
}
return C.RESULT_NOTHING_READ;
}
- if (formats[relativeReadIndex] != downstreamFormat) {
+ if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
formatHolder.format = formats[relativeReadIndex];
return C.RESULT_FORMAT_READ;
}
@@ -775,18 +796,22 @@ public final class DefaultTrackOutput implements TrackOutput {
}
/**
- * Attempts to locate the keyframe before the specified time, if it's present in the buffer.
+ * 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) {
+ public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
return C.POSITION_UNSET;
}
- if (timeUs > largestQueuedTimestampUs) {
+ if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
return C.POSITION_UNSET;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java
index 4de4b38897..a4da5d8e66 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java
@@ -51,6 +51,34 @@ public final class TimestampAdjuster {
lastSampleTimestamp = C.TIME_UNSET;
}
+ /**
+ * 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.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
index 2eac7926e7..c8ee8ff8c3 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -127,6 +127,7 @@ import java.util.List;
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");
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index 75d7cc555c..603aec4b22 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -20,6 +20,7 @@ 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;
@@ -44,6 +45,7 @@ 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;
@@ -73,7 +75,7 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
- FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED})
+ FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_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.
@@ -87,11 +89,16 @@ public final class FragmentedMp4Extractor implements Extractor {
* 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 {@link Track} was sideloaded, instead of being declared by the MP4
* container.
*/
- private static final int FLAG_SIDELOADED = 4;
+ private static final int FLAG_SIDELOADED = 8;
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};
@@ -123,6 +130,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
private final Stack containerAtoms;
+ private final LinkedList pendingMetadataSampleInfos;
private int parserState;
private int atomType;
@@ -130,8 +138,10 @@ public final class FragmentedMp4Extractor implements Extractor {
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;
@@ -139,6 +149,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// Extractor output.
private ExtractorOutput extractorOutput;
+ private TrackOutput eventMessageTrackOutput;
// Whether extractorOutput.seekMap has been called.
private boolean haveOutputSeekMap;
@@ -172,8 +183,10 @@ public final class FragmentedMp4Extractor implements Extractor {
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();
}
@@ -189,6 +202,7 @@ public final class FragmentedMp4Extractor implements Extractor {
TrackBundle bundle = new TrackBundle(output.track(0));
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
trackBundles.put(0, bundle);
+ maybeInitEventMessageTrack();
extractorOutput.endTracks();
}
}
@@ -199,6 +213,8 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) {
trackBundles.valueAt(i).reset();
}
+ pendingMetadataSampleInfos.clear();
+ pendingMetadataSampleBytes = 0;
containerAtoms.clear();
enterReadingAtomHeaderState();
}
@@ -336,9 +352,12 @@ public final class FragmentedMp4Extractor implements Extractor {
if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) {
- ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition);
- extractorOutput.seekMap(segmentIndex);
+ Pair result = parseSidx(leaf.data, inputPosition);
+ segmentIndexEarliestPresentationTimeUs = result.first;
+ extractorOutput.seekMap(result.second);
haveOutputSeekMap = true;
+ } else if (leaf.type == Atom.TYPE_emsg) {
+ onEmsgLeafAtomRead(leaf.data);
}
}
@@ -394,6 +413,7 @@ public final class FragmentedMp4Extractor implements Extractor {
trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i)));
durationUs = Math.max(durationUs, track.durationUs);
}
+ maybeInitEventMessageTrack();
extractorOutput.endTracks();
} else {
Assertions.checkState(trackBundles.size() == trackCount);
@@ -417,6 +437,47 @@ public final class FragmentedMp4Extractor implements Extractor {
}
}
+ private void maybeInitEventMessageTrack() {
+ if ((flags & FLAG_ENABLE_EMSG_TRACK) == 0) {
+ return;
+ }
+ eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
+ eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
+ Format.OFFSET_SAMPLE_RELATIVE));
+ }
+
+ /**
+ * 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).
*/
@@ -628,7 +689,7 @@ public final class FragmentedMp4Extractor implements Extractor {
DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
int defaultSampleDescriptionIndex =
((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
- ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
+ ? 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)
@@ -832,8 +893,13 @@ public final class FragmentedMp4Extractor implements Extractor {
/**
* 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 ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition)
+ private static Pair parseSidx(ParsableByteArray atom, long inputPosition)
throws ParserException {
atom.setPosition(Atom.HEADER_SIZE);
int fullAtom = atom.readInt();
@@ -850,6 +916,8 @@ public final class FragmentedMp4Extractor implements Extractor {
earliestPresentationTime = atom.readUnsignedLongToLong();
offset += atom.readUnsignedLongToLong();
}
+ long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
+ C.MICROS_PER_SECOND, timescale);
atom.skipBytes(2);
@@ -860,7 +928,7 @@ public final class FragmentedMp4Extractor implements Extractor {
long[] timesUs = new long[referenceCount];
long time = earliestPresentationTime;
- long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
+ long timeUs = earliestPresentationTimeUs;
for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt();
@@ -884,7 +952,8 @@ public final class FragmentedMp4Extractor implements Extractor {
offset += sizes[i];
}
- return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
+ return Pair.create(earliestPresentationTimeUs,
+ new ChunkIndex(sizes, offsets, durationsUs, timesUs));
}
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
@@ -946,13 +1015,9 @@ public final class FragmentedMp4Extractor implements Extractor {
// We skip bytes preceding the next sample to read.
int bytesToSkip = (int) (nextDataPosition - input.getPosition());
if (bytesToSkip < 0) {
- if (nextDataPosition == currentTrackBundle.fragment.atomPosition) {
- // Assume the sample data must be contiguous in the mdat with no preceeding data.
- Log.w(TAG, "Offset to sample data was missing.");
- bytesToSkip = 0;
- } else {
- throw new ParserException("Offset to sample data was negative.");
- }
+ // 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;
@@ -1029,6 +1094,14 @@ public final class FragmentedMp4Extractor implements Extractor {
}
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
@@ -1134,7 +1207,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|| 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_mehd || atom == Atom.TYPE_emsg;
}
/** Returns whether the extractor should decode a container atom with type {@code atom}. */
@@ -1144,6 +1217,21 @@ public final class FragmentedMp4Extractor implements Extractor {
|| 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.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
index e99dab053b..fed1694925 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util;
if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16);
- return new TextInformationFrame(id, value);
+ return new TextInformationFrame(id, null, value);
}
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
return null;
@@ -213,7 +213,7 @@ import com.google.android.exoplayer2.util.Util;
value = Math.min(1, value);
}
if (value >= 0) {
- return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value))
+ 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));
@@ -228,12 +228,12 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
int index = data.readUnsignedShort();
if (index > 0) {
- String description = "" + index;
+ String value = "" + index;
int count = data.readUnsignedShort();
if (count > 0) {
- description += "/" + count;
+ value += "/" + count;
}
- return new TextInformationFrame(attributeName, description);
+ return new TextInformationFrame(attributeName, null, value);
}
}
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
@@ -245,7 +245,7 @@ import com.google.android.exoplayer2.util.Util;
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null;
if (genreString != null) {
- return new TextInformationFrame("TCON", genreString);
+ return new TextInformationFrame("TCON", null, genreString);
}
Log.w(TAG, "Failed to parse standard genre code");
return null;
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
index f6cd29aff2..f9957aebe5 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
@@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor {
while (true) {
switch (parserState) {
case STATE_READING_HEADER:
- parseHeader(input);
- parserState = STATE_READING_TIMESTAMP_AND_COUNT;
+ 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)) {
@@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor {
// Do nothing
}
- private void parseHeader(ExtractorInput input) throws IOException, InterruptedException {
+ private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
dataScratch.reset();
- input.readFully(dataScratch.data, 0, HEADER_SIZE);
- if (dataScratch.readInt() != HEADER_ID) {
- throw new IOException("Input not RawCC");
+ 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;
}
- version = dataScratch.readUnsignedByte();
- // no versions use the flag fields yet
}
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
index b1e71d6651..121a622362 100644
--- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
+++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
@@ -28,11 +28,14 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
*/
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;
output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null,
Format.NO_VALUE, null));
@@ -40,9 +43,19 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
@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(0, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+ output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,
+ sampleSize, 0, null);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 6dce2abc2a..7e8b83b84c 100644
--- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -270,7 +270,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
- return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder, false);
+ return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
}
/**
diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
index ea8832c39c..bb946d76f9 100644
--- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
+++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
@@ -29,9 +29,9 @@ public interface MediaCodecSelector {
MediaCodecSelector DEFAULT = new MediaCodecSelector() {
@Override
- public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder,
- boolean requiresTunneling) throws DecoderQueryException {
- return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder, requiresTunneling);
+ public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+ throws DecoderQueryException {
+ return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
}
@Override
@@ -46,13 +46,11 @@ public interface MediaCodecSelector {
*
* @param mimeType The mime type for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required.
- * @param requiresTunneling Whether a decoder that supports tunneling is required.
- * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
- * exists.
+ * @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,
- boolean requiresTunneling) throws DecoderQueryException;
+ MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+ throws DecoderQueryException;
/**
* Selects a decoder to instantiate for audio passthrough.
diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index 14ba309790..a3a2543461 100644
--- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -81,9 +81,8 @@ public final class MediaCodecUtil {
/**
* Optional call to warm the codec cache for a given mime type.
*
- * Calling this method may speed up subsequent calls to
- * {@link #getDecoderInfo(String, boolean, boolean)} and
- * {@link #getDecoderInfos(String, boolean)}.
+ * 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
@@ -115,26 +114,14 @@ public final class MediaCodecUtil {
* @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.
- * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless
- * tunneling 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, boolean tunneling)
+ public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
throws DecoderQueryException {
List decoderInfos = getDecoderInfos(mimeType, secure);
- if (tunneling) {
- for (int i = 0; i < decoderInfos.size(); i++) {
- MediaCodecInfo decoderInfo = decoderInfos.get(i);
- if (decoderInfo.tunneling) {
- return decoderInfo;
- }
- }
- return null;
- } else {
- return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
- }
+ return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
}
/**
@@ -305,7 +292,7 @@ public final class MediaCodecUtil {
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
if (maxH264DecodableFrameSize == -1) {
int result = 0;
- MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false, false);
+ MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo != null) {
for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
index a73311f16b..9137bad4fd 100644
--- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
@@ -21,21 +21,12 @@ package com.google.android.exoplayer2.metadata;
public interface MetadataDecoder {
/**
- * Checks whether the decoder supports a given mime type.
+ * Decodes a {@link Metadata} element from the provided input buffer.
*
- * @param mimeType A metadata mime type.
- * @return Whether the mime type is supported.
- */
- boolean canDecode(String mimeType);
-
- /**
- * Decodes a metadata object from the provided binary data.
- *
- * @param data The raw binary data from which to decode the metadata.
- * @param size The size of the input data.
+ * @param inputBuffer The input buffer to decode.
* @return The decoded metadata object.
* @throws MetadataDecoderException If a problem occurred decoding the data.
*/
- Metadata decode(byte[] data, int size) throws MetadataDecoderException;
+ Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
new file mode 100644
index 0000000000..414a8269d7
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ *
+ * The formats supported by this factory are:
+ *
+ * - ID3 ({@link Id3Decoder})
+ * - EMSG ({@link EventMessageDecoder})
+ * - SCTE-35 ({@link SpliceInfoDecoder})
+ *
+ */
+ MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() {
+
+ @Override
+ public boolean supportsFormat(Format format) {
+ return getDecoderClass(format.sampleMimeType) != null;
+ }
+
+ @Override
+ public MetadataDecoder createDecoder(Format format) {
+ try {
+ Class> clazz = getDecoderClass(format.sampleMimeType);
+ if (clazz == null) {
+ throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
+ }
+ return clazz.asSubclass(MetadataDecoder.class).getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException("Unexpected error instantiating decoder", e);
+ }
+ }
+
+ private Class> getDecoderClass(String mimeType) {
+ if (mimeType == null) {
+ return null;
+ }
+ try {
+ switch (mimeType) {
+ case MimeTypes.APPLICATION_ID3:
+ return Class.forName("com.google.android.exoplayer2.metadata.id3.Id3Decoder");
+ case MimeTypes.APPLICATION_EMSG:
+ return Class.forName("com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder");
+ case MimeTypes.APPLICATION_SCTE35:
+ return Class.forName("com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder");
+ default:
+ return null;
+ }
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ };
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
new file mode 100644
index 0000000000..a09b565653
--- /dev/null
+++ b/library/src/main/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);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
index ff1364610b..550a13771f 100644
--- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -24,9 +24,7 @@ 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.DecoderInputBuffer;
import com.google.android.exoplayer2.util.Assertions;
-import java.nio.ByteBuffer;
/**
* A renderer for metadata.
@@ -49,12 +47,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private static final int MSG_INVOKE_RENDERER = 0;
- private final MetadataDecoder metadataDecoder;
+ private final MetadataDecoderFactory decoderFactory;
private final Output output;
private final Handler outputHandler;
private final FormatHolder formatHolder;
- private final DecoderInputBuffer buffer;
+ private final MetadataInputBuffer buffer;
+ private MetadataDecoder decoder;
private boolean inputStreamEnded;
private long pendingMetadataTimestamp;
private Metadata pendingMetadata;
@@ -66,21 +65,38 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
* 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 metadataDecoder A decoder for the metadata.
*/
- public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) {
+ 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.metadataDecoder = Assertions.checkNotNull(metadataDecoder);
+ this.decoderFactory = Assertions.checkNotNull(decoderFactory);
formatHolder = new FormatHolder();
- buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ buffer = new MetadataInputBuffer();
}
@Override
public int supportsFormat(Format format) {
- return metadataDecoder.canDecode(format.sampleMimeType) ? FORMAT_HANDLED
- : FORMAT_UNSUPPORTED_TYPE;
+ return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
+ }
+
+ @Override
+ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+ decoder = decoderFactory.createDecoder(formats[0]);
}
@Override
@@ -97,12 +113,16 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
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 {
pendingMetadataTimestamp = buffer.timeUs;
+ buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
+ buffer.flip();
try {
- buffer.flip();
- ByteBuffer bufferData = buffer.data;
- pendingMetadata = metadataDecoder.decode(bufferData.array(), bufferData.limit());
+ pendingMetadata = decoder.decode(buffer);
} catch (MetadataDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
@@ -119,6 +139,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
@Override
protected void onDisabled() {
pendingMetadata = null;
+ decoder = null;
super.onDisabled();
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
new file mode 100644
index 0000000000..9d6d0af60c
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.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.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 CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public EventMessage createFromParcel(Parcel in) {
+ return new EventMessage(in);
+ }
+
+ @Override
+ public EventMessage[] newArray(int size) {
+ return new EventMessage[size];
+ }
+
+ };
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
new file mode 100644
index 0000000000..fd6996aa80
--- /dev/null
+++ b/library/src/main/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.
+ *
+ * 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));
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
new file mode 100644
index 0000000000..c82f982aa7
--- /dev/null
+++ b/library/src/main/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 CREATOR = new Creator() {
+
+ @Override
+ public ChapterFrame createFromParcel(Parcel in) {
+ return new ChapterFrame(in);
+ }
+
+ @Override
+ public ChapterFrame[] newArray(int size) {
+ return new ChapterFrame[size];
+ }
+
+ };
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
new file mode 100644
index 0000000000..d71d0863c7
--- /dev/null
+++ b/library/src/main/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 CREATOR = new Creator() {
+
+ @Override
+ public ChapterTocFrame createFromParcel(Parcel in) {
+ return new ChapterTocFrame(in);
+ }
+
+ @Override
+ public ChapterTocFrame[] newArray(int size) {
+ return new ChapterTocFrame[size];
+ }
+
+ };
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
index d27c4f06e9..16059ccfbf 100644
--- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -16,12 +16,14 @@
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.util.MimeTypes;
+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;
@@ -49,11 +51,18 @@ public final class Id3Decoder implements MetadataDecoder {
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
@Override
- public boolean canDecode(String mimeType) {
- return mimeType.equals(MimeTypes.APPLICATION_ID3);
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = inputBuffer.data;
+ return decode(buffer.array(), buffer.limit());
}
- @Override
+ /**
+ * 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 id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size);
@@ -84,7 +93,8 @@ public final class Id3Decoder implements MetadataDecoder {
int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
while (id3Data.bytesLeft() >= frameHeaderSize) {
- Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack);
+ Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize);
if (frame != null) {
id3Frames.add(frame);
}
@@ -190,7 +200,7 @@ public final class Id3Decoder implements MetadataDecoder {
}
private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
- boolean unsignedIntFrameSizeHack) {
+ boolean unsignedIntFrameSizeHack, int frameHeaderSize) {
int frameId0 = id3Data.readUnsignedByte();
int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Data.readUnsignedByte();
@@ -266,6 +276,19 @@ public final class Id3Decoder implements MetadataDecoder {
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
&& (majorVersion == 2 || frameId3 == 'X')) {
frame = decodeTxxxFrame(id3Data, frameSize);
+ } else if (frameId0 == 'T') {
+ String id = majorVersion == 2
+ ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+ : String.format(Locale.US, "%c%c%c%c", 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 = majorVersion == 2
+ ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+ : String.format(Locale.US, "%c%c%c%c", 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'
@@ -274,14 +297,15 @@ public final class Id3Decoder implements MetadataDecoder {
} 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 == 'T') {
- String id = majorVersion == 2
- ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
- : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
- frame = decodeTextInformationFrame(id3Data, frameSize, id);
} 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);
+ } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
+ frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
+ frameHeaderSize);
} else {
String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
@@ -297,7 +321,7 @@ public final class Id3Decoder implements MetadataDecoder {
}
}
- private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
+ private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
@@ -308,11 +332,74 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
+ String value;
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
- int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
- String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
+ if (valueStartIndex < data.length) {
+ int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
+ value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
+ } else {
+ value = "";
+ }
- return new TxxxFrame(description, value);
+ return new TextInformationFrame("TXXX", description, value);
+ }
+
+ private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
+ int frameSize, String id) throws UnsupportedEncodingException {
+ if (frameSize <= 1) {
+ // Frame is empty or contains only the text encoding byte.
+ return new TextInformationFrame(id, 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 {
+ 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 {
+ if (frameSize == 0) {
+ // Frame is empty.
+ return new UrlLinkFrame(id, null, "");
+ }
+
+ 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)
@@ -408,25 +495,88 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
+ String text;
int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
- int textEndIndex = indexOfEos(data, textStartIndex, encoding);
- String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
+ 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 TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
- int frameSize, String id) throws UnsupportedEncodingException {
- int encoding = id3Data.readUnsignedByte();
- String charset = getCharsetName(encoding);
+ private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
+ int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize)
+ 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);
- byte[] data = new byte[frameSize - 1];
- id3Data.readBytes(data, 0, frameSize - 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;
+ }
- int descriptionEndIndex = indexOfEos(data, 0, encoding);
- String description = new String(data, 0, descriptionEndIndex, charset);
+ ArrayList subFrames = new ArrayList<>();
+ int limit = framePosition + frameSize;
+ while (id3Data.getPosition() < limit) {
+ Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize);
+ if (frame != null) {
+ subFrames.add(frame);
+ }
+ }
- return new TextInformationFrame(id, description);
+ 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)
+ 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 subFrames = new ArrayList<>();
+ int limit = framePosition + frameSize;
+ while (id3Data.getPosition() < limit) {
+ Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize);
+ 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,
@@ -458,6 +608,7 @@ public final class Id3Decoder implements MetadataDecoder {
/**
* Maps encoding byte from ID3v2 frame to a Charset.
+ *
* @param encodingByte The value of encoding byte from ID3v2 frame.
* @return Charset name.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
index b8c061fd0a..6221062e33 100644
--- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -20,20 +20,23 @@ import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
- * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
+ * Text information ID3 frame.
*/
public final class TextInformationFrame extends Id3Frame {
public final String description;
+ public final String value;
- public TextInformationFrame(String id, String description) {
+ 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
@@ -45,7 +48,8 @@ public final class TextInformationFrame extends Id3Frame {
return false;
}
TextInformationFrame other = (TextInformationFrame) obj;
- return id.equals(other.id) && Util.areEqual(description, other.description);
+ return id.equals(other.id) && Util.areEqual(description, other.description)
+ && Util.areEqual(value, other.value);
}
@Override
@@ -53,6 +57,7 @@ public final class TextInformationFrame extends Id3Frame {
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;
}
@@ -60,6 +65,7 @@ public final class TextInformationFrame extends Id3Frame {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(description);
+ dest.writeString(value);
}
public static final Parcelable.Creator CREATOR =
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
similarity index 55%
rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java
rename to library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
index 5c24e70ef4..2148b921e1 100644
--- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/TxxxFrame.java
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -20,25 +20,23 @@ import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
- * TXXX (User defined text information) ID3 frame.
+ * Url link ID3 frame.
*/
-public final class TxxxFrame extends Id3Frame {
-
- public static final String ID = "TXXX";
+public final class UrlLinkFrame extends Id3Frame {
public final String description;
- public final String value;
+ public final String url;
- public TxxxFrame(String description, String value) {
- super(ID);
+ public UrlLinkFrame(String id, String description, String url) {
+ super(id);
this.description = description;
- this.value = value;
+ this.url = url;
}
- /* package */ TxxxFrame(Parcel in) {
- super(ID);
+ /* package */ UrlLinkFrame(Parcel in) {
+ super(in.readString());
description = in.readString();
- value = in.readString();
+ url = in.readString();
}
@Override
@@ -49,36 +47,40 @@ public final class TxxxFrame extends Id3Frame {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
- TxxxFrame other = (TxxxFrame) obj;
- return Util.areEqual(description, other.description) && Util.areEqual(value, other.value);
+ 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 + (value != null ? value.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(value);
+ dest.writeString(url);
}
- public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
- @Override
- public TxxxFrame createFromParcel(Parcel in) {
- return new TxxxFrame(in);
- }
+ @Override
+ public UrlLinkFrame createFromParcel(Parcel in) {
+ return new UrlLinkFrame(in);
+ }
- @Override
- public TxxxFrame[] newArray(int size) {
- return new TxxxFrame[size];
- }
+ @Override
+ public UrlLinkFrame[] newArray(int size) {
+ return new UrlLinkFrame[size];
+ }
- };
+ };
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
index 5af0f25481..6e373a45e7 100644
--- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
+++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
@@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.metadata.scte35;
-import android.text.TextUtils;
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.util.MimeTypes;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
/**
* Decodes splice info sections and produces splice commands.
@@ -43,12 +43,10 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
}
@Override
- public boolean canDecode(String mimeType) {
- return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35);
- }
-
- @Override
- public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
+ public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
+ 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),
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
index c39bccda3d..b18eabf493 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -26,10 +26,12 @@ import java.io.IOException;
* Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their
* samples.
*/
-/* package */ final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+ /**
+ * The {@link MediaPeriod} wrapped by this clipping media period.
+ */
public final MediaPeriod mediaPeriod;
- private final ClippingMediaSource mediaSource;
private MediaPeriod.Callback callback;
private long startUs;
@@ -40,18 +42,31 @@ import java.io.IOException;
/**
* Creates a new clipping media period that provides a clipped view of the specified
* {@link MediaPeriod}'s sample streams.
+ *
+ * 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.
- * @param mediaSource The {@link ClippingMediaSource} to which this period belongs.
*/
- public ClippingMediaPeriod(MediaPeriod mediaPeriod, ClippingMediaSource mediaSource) {
+ public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
this.mediaPeriod = mediaPeriod;
- this.mediaSource = mediaSource;
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;
@@ -80,7 +95,8 @@ import java.io.IOException;
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
internalStreams, streamResetFlags, positionUs + startUs);
Assertions.checkState(enablePositionUs == positionUs + startUs
- || (enablePositionUs >= startUs && enablePositionUs <= endUs));
+ || (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;
@@ -110,14 +126,16 @@ import java.io.IOException;
if (discontinuityUs == C.TIME_UNSET) {
return C.TIME_UNSET;
}
- Assertions.checkState(discontinuityUs >= startUs && discontinuityUs <= endUs);
+ 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 || bufferedPositionUs >= endUs) {
+ 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);
@@ -131,14 +149,16 @@ import java.io.IOException;
}
}
long seekUs = mediaPeriod.seekToUs(positionUs + startUs);
- Assertions.checkState(seekUs == positionUs + startUs || (seekUs >= startUs && seekUs <= endUs));
+ 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 || nextLoadPositionUs >= endUs) {
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE
+ || (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {
return C.TIME_END_OF_SOURCE;
}
return nextLoadPositionUs - startUs;
@@ -153,8 +173,6 @@ import java.io.IOException;
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
- startUs = mediaSource.getStartUs();
- endUs = mediaSource.getEndUs();
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
@@ -217,21 +235,24 @@ import java.io.IOException;
if (pendingDiscontinuity) {
return C.RESULT_NOTHING_READ;
}
+ if (buffer == null) {
+ return stream.readData(formatHolder, null);
+ }
if (sentEos) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
int result = stream.readData(formatHolder, buffer);
// TODO: Clear gapless playback metadata if a format was read (if applicable).
- if ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
- || (result == C.RESULT_NOTHING_READ
- && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE)) {
+ 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) {
+ if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
buffer.timeUs -= startUs;
}
return result;
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index e92dce8231..be15a07726 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -21,17 +21,19 @@ 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). The specified start position must correspond to a synchronization sample in the period.
+ * (live).
*/
public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
private final MediaSource mediaSource;
private final long startUs;
private final long endUs;
+ private final ArrayList mediaPeriods;
private MediaSource.Listener sourceListener;
private ClippingTimeline clippingTimeline;
@@ -51,20 +53,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
this.mediaSource = Assertions.checkNotNull(mediaSource);
startUs = startPositionUs;
endUs = endPositionUs;
- }
-
- /**
- * Returns the start position of the clipping source's timeline in microseconds.
- */
- /* package */ long getStartUs() {
- return clippingTimeline.startUs;
- }
-
- /**
- * Returns the end position of the clipping source's timeline in microseconds.
- */
- /* package */ long getEndUs() {
- return clippingTimeline.endUs;
+ mediaPeriods = new ArrayList<>();
}
@Override
@@ -80,12 +69,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
@Override
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
- return new ClippingMediaPeriod(
- mediaSource.createPeriod(index, allocator, startUs + positionUs), this);
+ 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);
}
@@ -100,6 +93,13 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
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);
+ }
}
/**
@@ -112,7 +112,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
private final long endUs;
/**
- * Creates a new timeline that wraps the specified timeline.
+ * 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}.
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
index 8ab4d45c47..bc0a3f1cf8 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -40,6 +40,7 @@ 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;
@@ -62,6 +63,7 @@ import java.io.IOException;
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;
@@ -101,11 +103,13 @@ import java.io.IOException;
* @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) {
+ Allocator allocator, String customCacheKey) {
this.uri = uri;
this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount;
@@ -113,6 +117,7 @@ import java.io.IOException;
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();
@@ -615,7 +620,7 @@ import java.io.IOException;
ExtractorInput input = null;
try {
long position = positionHolder.position;
- length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, null));
+ length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey));
if (length != C.LENGTH_UNSET) {
length += position;
}
@@ -640,7 +645,7 @@ import java.io.IOException;
} else if (input != null) {
positionHolder.position = input.getPosition();
}
- dataSource.close();
+ Util.closeQuietly(dataSource);
}
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
index 559d241598..7b571bc289 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -93,6 +93,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
private final Handler eventHandler;
private final EventListener eventListener;
private final Timeline.Period period;
+ private final String customCacheKey;
private MediaSource.Listener sourceListener;
private Timeline timeline;
@@ -110,7 +111,25 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
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);
+ 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);
}
/**
@@ -122,16 +141,19 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
* @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) {
+ 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();
}
@@ -152,7 +174,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
Assertions.checkArgument(index == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
- this, allocator);
+ this, allocator, customCacheKey);
}
@Override
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index f4a9665b10..31ee8df1e4 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -16,6 +16,7 @@
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;
@@ -47,6 +48,10 @@ public interface MediaPeriod extends SequenceableLoader {
*
* {@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
+ *
+ * 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.
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java
index 39374acb33..5ee70cd2ed 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/SampleStream.java
@@ -44,11 +44,17 @@ public interface SampleStream {
/**
* Attempts to read from the stream.
+ *
+ * If no data is available then {@link C#RESULT_NOTHING_READ} is returned. If the format of the
+ * media is changing or if {@code buffer == null} 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.
+ * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
+ * caller requires that the format of the stream be read even if it's not changing.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 1ad448bd12..c78bb5371b 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -28,6 +28,7 @@ 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;
@@ -205,13 +206,13 @@ import java.util.Arrays;
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
- if (streamState == STREAM_STATE_END_OF_STREAM) {
- buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
- return C.RESULT_BUFFER_READ;
- } else if (streamState == STREAM_STATE_SEND_FORMAT) {
+ if (buffer == null || streamState == STREAM_STATE_SEND_FORMAT) {
formatHolder.format = format;
streamState = STREAM_STATE_SEND_SAMPLE;
return C.RESULT_FORMAT_READ;
+ } else if (streamState == STREAM_STATE_END_OF_STREAM) {
+ buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ return C.RESULT_BUFFER_READ;
}
Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE);
@@ -276,7 +277,7 @@ import java.util.Arrays;
result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
}
} finally {
- dataSource.close();
+ Util.closeQuietly(dataSource);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
index ed76a505ea..2623d31cef 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -30,15 +30,15 @@ import java.io.IOException;
/**
* An {@link Extractor} wrapper for loading chunks containing a single track.
*
- * The wrapper allows switching of the {@link SingleTrackMetadataOutput} and {@link TrackOutput}
- * which receive parsed data.
+ * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive
+ * parsed data.
*/
public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput {
/**
- * Receives metadata associated with the track as extracted by the wrapped {@link Extractor}.
+ * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}.
*/
- public interface SingleTrackMetadataOutput {
+ public interface SeekMapOutput {
/**
* @see ExtractorOutput#seekMap(SeekMap)
@@ -47,13 +47,14 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
}
- private final Extractor extractor;
+ public final Extractor extractor;
+
private final Format manifestFormat;
private final boolean preferManifestDrmInitData;
private final boolean resendFormatOnInit;
private boolean extractorInitialized;
- private SingleTrackMetadataOutput metadataOutput;
+ private SeekMapOutput seekMapOutput;
private TrackOutput trackOutput;
private Format sentFormat;
@@ -68,7 +69,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
* @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat}
* should be preferred when the sample and manifest {@link Format}s are merged.
* @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when
- * it is initialized via {@link #init(SingleTrackMetadataOutput, TrackOutput)}.
+ * it is initialized via {@link #init(SeekMapOutput, TrackOutput)}.
*/
public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat,
boolean preferManifestDrmInitData, boolean resendFormatOnInit) {
@@ -79,14 +80,14 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
}
/**
- * Initializes the extractor to output to the provided {@link SingleTrackMetadataOutput} and
+ * Initializes the extractor to output to the provided {@link SeekMapOutput} and
* {@link TrackOutput} instances, and configures it to receive data from a new chunk.
*
- * @param metadataOutput The {@link SingleTrackMetadataOutput} that will receive metadata.
+ * @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s.
* @param trackOutput The {@link TrackOutput} that will receive sample data.
*/
- public void init(SingleTrackMetadataOutput metadataOutput, TrackOutput trackOutput) {
- this.metadataOutput = metadataOutput;
+ public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) {
+ this.seekMapOutput = seekMapOutput;
this.trackOutput = trackOutput;
if (!extractorInitialized) {
extractor.init(this);
@@ -99,20 +100,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
}
}
- /**
- * Reads from the provided {@link ExtractorInput}.
- *
- * @param input The {@link ExtractorInput} from which to read.
- * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
- * @throws IOException If an error occurred reading from the source.
- * @throws InterruptedException If the thread was interrupted.
- */
- public int read(ExtractorInput input) throws IOException, InterruptedException {
- int result = extractor.read(input, null);
- Assertions.checkState(result != Extractor.RESULT_SEEK);
- return result;
- }
-
// ExtractorOutput implementation.
@Override
@@ -130,7 +117,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
@Override
public void seekMap(SeekMap seekMap) {
- metadataOutput.seekMap(seekMap);
+ seekMapOutput.seekMap(seekMap);
}
// TrackOutput implementation.
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index 6de7c6ec01..3955d64034 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -122,7 +122,8 @@ public class ChunkSampleStream implements SampleStream, S
public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the sample queue.
- boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs);
+ boolean seekInsideBuffer = !isPendingReset()
+ && sampleQueue.skipToKeyframeBefore(positionUs, positionUs < getNextLoadPositionUs());
if (seekInsideBuffer) {
// We succeeded. All we need to do is discard any chunks that we've moved past.
while (mediaChunks.size() > 1
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
index a5af3cc42f..060e6130cf 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -21,16 +21,17 @@ import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
-import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SingleTrackMetadataOutput;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
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 implements SingleTrackMetadataOutput {
+public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput {
private final int chunkCount;
private final long sampleOffsetUs;
@@ -85,7 +86,7 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe
return bytesLoaded;
}
- // SingleTrackMetadataOutput implementation.
+ // SeekMapOutput implementation.
@Override
public final void seekMap(SeekMap seekMap) {
@@ -120,15 +121,17 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe
}
// Load and decode the sample data.
try {
+ Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
- result = extractorWrapper.read(input);
+ result = extractor.read(input, null);
}
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
- dataSource.close();
+ Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java
index 99653d323f..0846e7679d 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java
@@ -19,6 +19,7 @@ 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;
@@ -96,7 +97,7 @@ public abstract class DataChunk extends Chunk {
consume(data, limit);
}
} finally {
- dataSource.close();
+ Util.closeQuietly(dataSource);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
index 388dc63899..c8c3389830 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -22,9 +22,10 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
-import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SingleTrackMetadataOutput;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
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.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
@@ -32,7 +33,7 @@ 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 implements SingleTrackMetadataOutput,
+public final class InitializationChunk extends Chunk implements SeekMapOutput,
TrackOutput {
private final ChunkExtractorWrapper extractorWrapper;
@@ -85,7 +86,7 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
return seekMap;
}
- // SingleTrackMetadataOutput implementation.
+ // SeekMapOutput implementation.
@Override
public void seekMap(SeekMap seekMap) {
@@ -142,15 +143,17 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
}
// Load and decode the initialization data.
try {
+ Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
- result = extractorWrapper.read(input);
+ result = extractor.read(input, null);
}
+ Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
- dataSource.close();
+ Util.closeQuietly(dataSource);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
index 3033566950..d7be74535e 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -98,7 +98,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
int sampleSize = bytesLoaded;
trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
} finally {
- dataSource.close();
+ Util.closeQuietly(dataSource);
}
loadCompleted = true;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java
index 9e48bc2c79..56ea626120 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java
@@ -28,9 +28,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
/**
* @param chunkIndex The {@link ChunkIndex} to wrap.
- * @param uri The URI where the data is located.
*/
- public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) {
+ public DashWrappingSegmentIndex(ChunkIndex chunkIndex) {
this.chunkIndex = chunkIndex;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
index 0e3d127796..74d53d3e32 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
@@ -185,10 +185,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make.
- Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource,
+ out.chunk = newInitializationChunk(representationHolder, dataSource,
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);
- out.chunk = initializationChunk;
return;
}
@@ -233,10 +232,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
- Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource,
- trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
- trackSelection.getSelectionData(), sampleFormat, segmentNum, maxSegmentCount);
- out.chunk = nextMediaChunk;
+ out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(),
+ trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat,
+ segmentNum, maxSegmentCount);
}
@Override
@@ -255,8 +253,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
if (representationHolder.segmentIndex == null) {
SeekMap seekMap = initializationChunk.getSeekMap();
if (seekMap != null) {
- representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap,
- initializationChunk.dataSpec.uri.toString());
+ representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap);
}
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
index 44da52f52c..c4a4a4446b 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
@@ -23,14 +23,35 @@ import java.util.List;
*/
public class AdaptationSet {
- public static final int UNSET_ID = -1;
+ /**
+ * Value of {@link #id} indicating no value is set.=
+ */
+ public static final int ID_UNSET = -1;
+ /**
+ * A non-negative identifier for the adaptation set that's unique in the scope of its containing
+ * period, or {@link #ID_UNSET} if not specified.
+ */
public final int id;
+ /**
+ * The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}
+ * {@code TRACK_TYPE_*} constants.
+ */
public final int type;
+ /**
+ * The {@link Representation}s in the adaptation set.
+ */
public final List representations;
+ /**
+ * @param id A non-negative identifier for the adaptation set that's unique in the scope of its
+ * containing period, or {@link #ID_UNSET} if not specified.
+ * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}
+ * {@code TRACK_TYPE_*} constants.
+ * @param representations The {@link Representation}s in the adaptation set.
+ */
public AdaptationSet(int id, int type, List representations) {
this.id = id;
this.type = type;
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index 7e2ce0de1d..a9dc0a8665 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -227,7 +227,7 @@ public class DashManifestParser extends DefaultHandler
protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl,
SegmentBase segmentBase) throws XmlPullParserException, IOException {
- int id = parseInt(xpp, "id", AdaptationSet.UNSET_ID);
+ int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET);
int contentType = parseContentType(xpp);
String mimeType = xpp.getAttributeValue(null, "mimeType");
@@ -240,7 +240,9 @@ public class DashManifestParser extends DefaultHandler
String language = xpp.getAttributeValue(null, "lang");
int accessibilityChannel = Format.NO_VALUE;
ArrayList drmSchemeDatas = new ArrayList<>();
+ ArrayList inbandEventStreams = new ArrayList<>();
List representationInfos = new ArrayList<>();
+ @C.SelectionFlags int selectionFlags = 0;
boolean seenFirstBaseUrl = false;
do {
@@ -258,32 +260,37 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) {
language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang"));
contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));
- } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
- RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs,
- width, height, frameRate, audioChannels, audioSamplingRate, language,
- accessibilityChannel, segmentBase);
- contentType = checkContentTypeConsistency(contentType,
- getContentType(representationInfo.format));
- representationInfos.add(representationInfo);
+ } else if (XmlPullParserUtil.isStartTag(xpp, "Role")) {
+ selectionFlags |= parseRole(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) {
accessibilityChannel = parseAccessibilityValue(xpp);
+ } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
+ RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs,
+ width, height, frameRate, audioChannels, audioSamplingRate, language,
+ accessibilityChannel, selectionFlags, segmentBase);
+ contentType = checkContentTypeConsistency(contentType,
+ getContentType(representationInfo.format));
+ representationInfos.add(representationInfo);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
+ } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
+ inbandEventStreams.add(parseInbandEventStream(xpp));
} else if (XmlPullParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet"));
+ // Build the representations.
List representations = new ArrayList<>(representationInfos.size());
for (int i = 0; i < representationInfos.size(); i++) {
representations.add(buildRepresentation(representationInfos.get(i), contentId,
- drmSchemeDatas));
+ drmSchemeDatas, inbandEventStreams));
}
return buildAdaptationSet(id, contentType, representations);
@@ -311,8 +318,7 @@ public class DashManifestParser extends DefaultHandler
return C.TRACK_TYPE_VIDEO;
} else if (MimeTypes.isAudio(sampleMimeType)) {
return C.TRACK_TYPE_AUDIO;
- } else if (mimeTypeIsRawText(sampleMimeType)
- || MimeTypes.APPLICATION_RAWCC.equals(format.containerMimeType)) {
+ } else if (mimeTypeIsRawText(sampleMimeType)) {
return C.TRACK_TYPE_TEXT;
}
return C.TRACK_TYPE_UNKNOWN;
@@ -355,6 +361,42 @@ public class DashManifestParser extends DefaultHandler
}
}
+ /**
+ * Parses an InbandEventStream element.
+ *
+ * @param xpp The parser from which to read.
+ * @throws XmlPullParserException If an error occurs parsing the element.
+ * @throws IOException If an error occurs reading the element.
+ * @return {@link InbandEventStream} parsed from the element.
+ */
+ protected InbandEventStream parseInbandEventStream(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+ String value = parseString(xpp, "value", null);
+ do {
+ xpp.next();
+ } while (!XmlPullParserUtil.isEndTag(xpp, "InbandEventStream"));
+ return new InbandEventStream(schemeIdUri, value);
+ }
+
+ /**
+ * Parses a Role element.
+ *
+ * @param xpp The parser from which to read.
+ * @throws XmlPullParserException If an error occurs parsing the element.
+ * @throws IOException If an error occurs reading the element.
+ * @return {@link C.SelectionFlags} parsed from the element.
+ */
+ protected int parseRole(XmlPullParser xpp) throws XmlPullParserException, IOException {
+ String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+ String value = parseString(xpp, "value", null);
+ do {
+ xpp.next();
+ } while (!XmlPullParserUtil.isEndTag(xpp, "Role"));
+ return "urn:mpeg:dash:role:2011".equals(schemeIdUri) && "main".equals(value)
+ ? C.SELECTION_FLAG_DEFAULT : 0;
+ }
+
/**
* Parses children of AdaptationSet elements not specifically parsed elsewhere.
*
@@ -373,8 +415,8 @@ public class DashManifestParser extends DefaultHandler
String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth,
int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels,
int adaptationSetAudioSamplingRate, String adaptationSetLanguage,
- int adaptationSetAccessibilityChannel, SegmentBase segmentBase)
- throws XmlPullParserException, IOException {
+ int adaptationSetAccessibilityChannel, @C.SelectionFlags int adaptationSetSelectionFlags,
+ SegmentBase segmentBase) throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE);
@@ -386,6 +428,7 @@ public class DashManifestParser extends DefaultHandler
int audioChannels = adaptationSetAudioChannels;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate);
ArrayList drmSchemeDatas = new ArrayList<>();
+ ArrayList inbandEventStreams = new ArrayList<>();
boolean seenFirstBaseUrl = false;
do {
@@ -408,52 +451,52 @@ public class DashManifestParser extends DefaultHandler
if (contentProtection != null) {
drmSchemeDatas.add(contentProtection);
}
+ } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
+ inbandEventStreams.add(parseInbandEventStream(xpp));
}
} while (!XmlPullParserUtil.isEndTag(xpp, "Representation"));
Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels,
audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetAccessibilityChannel,
- codecs);
+ adaptationSetSelectionFlags, codecs);
segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();
- return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas);
+ return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas, inbandEventStreams);
}
protected Format buildFormat(String id, String containerMimeType, int width, int height,
float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language,
- int accessiblityChannel, String codecs) {
+ int accessiblityChannel, @C.SelectionFlags int selectionFlags, String codecs) {
String sampleMimeType = getSampleMimeType(containerMimeType, codecs);
if (sampleMimeType != null) {
if (MimeTypes.isVideo(sampleMimeType)) {
return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs,
- bitrate, width, height, frameRate, null);
+ bitrate, width, height, frameRate, null, selectionFlags);
} else if (MimeTypes.isAudio(sampleMimeType)) {
return Format.createAudioContainerFormat(id, containerMimeType, sampleMimeType, codecs,
- bitrate, audioChannels, audioSamplingRate, null, 0, language);
+ bitrate, audioChannels, audioSamplingRate, null, selectionFlags, language);
} else if (mimeTypeIsRawText(sampleMimeType)) {
return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs,
- bitrate, 0, language, accessiblityChannel);
- } else if (containerMimeType.equals(MimeTypes.APPLICATION_RAWCC)) {
- return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs,
- bitrate, 0, language, accessiblityChannel);
- } else {
- return Format.createContainerFormat(id, containerMimeType, codecs, sampleMimeType, bitrate);
+ bitrate, selectionFlags, language, accessiblityChannel);
}
- } else {
- return Format.createContainerFormat(id, containerMimeType, codecs, sampleMimeType, bitrate);
}
+ return Format.createContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
+ selectionFlags, language);
}
protected Representation buildRepresentation(RepresentationInfo representationInfo,
- String contentId, ArrayList extraDrmSchemeDatas) {
+ String contentId, ArrayList extraDrmSchemeDatas,
+ ArrayList extraInbandEventStreams) {
Format format = representationInfo.format;
ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas));
}
+ ArrayList inbandEventStremas = representationInfo.inbandEventStreams;
+ inbandEventStremas.addAll(extraInbandEventStreams);
return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format,
- representationInfo.baseUrl, representationInfo.segmentBase);
+ representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas);
}
// SegmentBase, SegmentList and SegmentTemplate parsing.
@@ -664,6 +707,14 @@ public class DashManifestParser extends DefaultHandler
return MimeTypes.getAudioMediaMimeType(codecs);
} else if (MimeTypes.isVideo(containerMimeType)) {
return MimeTypes.getVideoMediaMimeType(codecs);
+ } else if (mimeTypeIsRawText(containerMimeType)) {
+ return containerMimeType;
+ } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {
+ if ("stpp".equals(codecs)) {
+ return MimeTypes.APPLICATION_TTML;
+ } else if ("wvtt".equals(codecs)) {
+ return MimeTypes.APPLICATION_MP4VTT;
+ }
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
if (codecs != null) {
if (codecs.contains("cea708")) {
@@ -673,14 +724,6 @@ public class DashManifestParser extends DefaultHandler
}
}
return null;
- } else if (mimeTypeIsRawText(containerMimeType)) {
- return containerMimeType;
- } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {
- if ("stpp".equals(codecs)) {
- return MimeTypes.APPLICATION_TTML;
- } else if ("wvtt".equals(codecs)) {
- return MimeTypes.APPLICATION_MP4VTT;
- }
}
return null;
}
@@ -692,7 +735,11 @@ public class DashManifestParser extends DefaultHandler
* @return Whether the mimeType is a text sample mimeType.
*/
private static boolean mimeTypeIsRawText(String mimeType) {
- return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType);
+ return MimeTypes.isText(mimeType)
+ || MimeTypes.APPLICATION_TTML.equals(mimeType)
+ || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(mimeType)
+ || MimeTypes.APPLICATION_CEA608.equals(mimeType);
}
/**
@@ -850,13 +897,15 @@ public class DashManifestParser extends DefaultHandler
public final String baseUrl;
public final SegmentBase segmentBase;
public final ArrayList drmSchemeDatas;
+ public final ArrayList inbandEventStreams;
public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase,
- ArrayList drmSchemeDatas) {
+ ArrayList drmSchemeDatas, ArrayList inbandEventStreams) {
this.format = format;
this.baseUrl = baseUrl;
this.segmentBase = segmentBase;
this.drmSchemeDatas = drmSchemeDatas;
+ this.inbandEventStreams = inbandEventStreams;
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/InbandEventStream.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/InbandEventStream.java
new file mode 100644
index 0000000000..2f24603598
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/InbandEventStream.java
@@ -0,0 +1,51 @@
+/*
+ * 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.source.dash.manifest;
+
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Represents a DASH in-band event stream.
+ */
+public class InbandEventStream {
+
+ public final String schemeIdUri;
+ public final String value;
+
+ public InbandEventStream(String schemeIdUri, String value) {
+ this.schemeIdUri = schemeIdUri;
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ InbandEventStream other = (InbandEventStream) obj;
+ return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0)
+ + (value != null ? value.hashCode() : 0);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
index f52727c1a8..cdf84f5f71 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java
@@ -21,6 +21,8 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.MultiSegmentBase;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
+import java.util.Collections;
+import java.util.List;
/**
* A DASH representation.
@@ -60,6 +62,10 @@ public abstract class Representation {
* The offset of the presentation timestamps in the media stream relative to media time.
*/
public final long presentationTimeOffsetUs;
+ /**
+ * The {@link InbandEventStream}s in the representation. Never null, but may be empty.
+ */
+ public final List inbandEventStreams;
private final RangedUri initializationUri;
@@ -78,6 +84,23 @@ public abstract class Representation {
return newInstance(contentId, revisionId, format, baseUrl, segmentBase, null);
}
+ /**
+ * Constructs a new instance.
+ *
+ * @param contentId Identifies the piece of content to which this representation belongs.
+ * @param revisionId Identifies the revision of the content.
+ * @param format The format of the representation.
+ * @param baseUrl The base URL.
+ * @param segmentBase A segment base element for the representation.
+ * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null.
+ * @return The constructed instance.
+ */
+ public static Representation newInstance(String contentId, long revisionId, Format format,
+ String baseUrl, SegmentBase segmentBase, List inbandEventStreams) {
+ return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams,
+ null);
+ }
+
/**
* Constructs a new instance.
*
@@ -86,18 +109,20 @@ public abstract class Representation {
* @param format The format of the representation.
* @param baseUrl The base URL of the representation.
* @param segmentBase A segment base element for the representation.
+ * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null.
* @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. This
* parameter is ignored if {@code segmentBase} consists of multiple segments.
* @return The constructed instance.
*/
public static Representation newInstance(String contentId, long revisionId, Format format,
- String baseUrl, SegmentBase segmentBase, String customCacheKey) {
+ String baseUrl, SegmentBase segmentBase, List inbandEventStreams,
+ String customCacheKey) {
if (segmentBase instanceof SingleSegmentBase) {
return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl,
- (SingleSegmentBase) segmentBase, customCacheKey, C.LENGTH_UNSET);
+ (SingleSegmentBase) segmentBase, inbandEventStreams, customCacheKey, C.LENGTH_UNSET);
} else if (segmentBase instanceof MultiSegmentBase) {
return new MultiSegmentRepresentation(contentId, revisionId, format, baseUrl,
- (MultiSegmentBase) segmentBase);
+ (MultiSegmentBase) segmentBase, inbandEventStreams);
} else {
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+ "MultiSegmentBase");
@@ -105,11 +130,14 @@ public abstract class Representation {
}
private Representation(String contentId, long revisionId, Format format, String baseUrl,
- SegmentBase segmentBase) {
+ SegmentBase segmentBase, List inbandEventStreams) {
this.contentId = contentId;
this.revisionId = revisionId;
this.format = format;
this.baseUrl = baseUrl;
+ this.inbandEventStreams = inbandEventStreams == null
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(inbandEventStreams);
initializationUri = segmentBase.getInitialization(this);
presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();
}
@@ -167,18 +195,20 @@ public abstract class Representation {
* @param initializationEnd The offset of the last byte of initialization data.
* @param indexStart The offset of the first byte of index data.
* @param indexEnd The offset of the last byte of index data.
+ * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null.
* @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null.
* @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.
*/
public static SingleSegmentRepresentation newInstance(String contentId, long revisionId,
Format format, String uri, long initializationStart, long initializationEnd,
- long indexStart, long indexEnd, String customCacheKey, long contentLength) {
+ long indexStart, long indexEnd, List inbandEventStreams,
+ String customCacheKey, long contentLength) {
RangedUri rangedUri = new RangedUri(null, initializationStart,
initializationEnd - initializationStart + 1);
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart,
indexEnd - indexStart + 1);
return new SingleSegmentRepresentation(contentId, revisionId,
- format, uri, segmentBase, customCacheKey, contentLength);
+ format, uri, segmentBase, inbandEventStreams, customCacheKey, contentLength);
}
/**
@@ -187,12 +217,14 @@ public abstract class Representation {
* @param format The format of the representation.
* @param baseUrl The base URL of the representation.
* @param segmentBase The segment base underlying the representation.
+ * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null.
* @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null.
* @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.
*/
public SingleSegmentRepresentation(String contentId, long revisionId, Format format,
- String baseUrl, SingleSegmentBase segmentBase, String customCacheKey, long contentLength) {
- super(contentId, revisionId, format, baseUrl, segmentBase);
+ String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams,
+ String customCacheKey, long contentLength) {
+ super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams);
this.uri = Uri.parse(baseUrl);
this.indexUri = segmentBase.getIndex();
this.cacheKey = customCacheKey != null ? customCacheKey
@@ -235,10 +267,11 @@ public abstract class Representation {
* @param format The format of the representation.
* @param baseUrl The base URL of the representation.
* @param segmentBase The segment base underlying the representation.
+ * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null.
*/
public MultiSegmentRepresentation(String contentId, long revisionId, Format format,
- String baseUrl, MultiSegmentBase segmentBase) {
- super(contentId, revisionId, format, baseUrl, segmentBase);
+ String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) {
+ super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams);
this.segmentBase = segmentBase;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
index ef319d508d..70a65e932a 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
@@ -143,7 +143,7 @@ public abstract class SegmentBase {
} else {
// The high index cannot be unbounded. Identify the segment using binary search.
while (lowIndex <= highIndex) {
- int midIndex = (lowIndex + highIndex) / 2;
+ int midIndex = lowIndex + (highIndex - lowIndex) / 2;
long midTimeUs = getSegmentTimeUs(midIndex);
if (midTimeUs < timeUs) {
lowIndex = midIndex + 1;
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
index b953fcf79c..edd3c735c1 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -270,8 +270,10 @@ import java.util.Locale;
// Compute start time of the next chunk.
long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
+ int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ + segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
- segment.discontinuitySequenceNumber, startTimeUs);
+ discontinuitySequence, startTimeUs);
// Configure the data source and spec for the chunk.
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
@@ -279,9 +281,8 @@ import java.util.Locale;
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
- startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
- segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
- encryptionKey, encryptionIv);
+ startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
+ isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv);
}
/**
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
index f9dba14e0e..0c411854d5 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -79,8 +79,10 @@ import java.util.concurrent.atomic.AtomicInteger;
private final boolean isEncrypted;
private final boolean isMasterTimestampSource;
private final TimestampAdjuster timestampAdjuster;
- private final HlsMediaChunk previousChunk;
private final String lastPathSegment;
+ private final Extractor previousExtractor;
+ private final boolean shouldSpliceIn;
+ private final boolean needNewExtractor;
private final boolean isPackedAudio;
private final Id3Decoder id3Decoder;
@@ -123,7 +125,6 @@ import java.util.concurrent.atomic.AtomicInteger;
this.isMasterTimestampSource = isMasterTimestampSource;
this.timestampAdjuster = timestampAdjuster;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
- this.previousChunk = previousChunk;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
lastPathSegment = dataSpec.uri.getLastPathSegment();
@@ -131,13 +132,19 @@ import java.util.concurrent.atomic.AtomicInteger;
|| lastPathSegment.endsWith(AC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(EC3_FILE_EXTENSION)
|| lastPathSegment.endsWith(MP3_FILE_EXTENSION);
- if (isPackedAudio) {
- id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder();
- id3Data = previousChunk != null ? previousChunk.id3Data
- : new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
+ if (previousChunk != null) {
+ id3Decoder = previousChunk.id3Decoder;
+ id3Data = previousChunk.id3Data;
+ previousExtractor = previousChunk.extractor;
+ shouldSpliceIn = previousChunk.hlsUrl != hlsUrl;
+ needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
+ || shouldSpliceIn;
} else {
- id3Decoder = null;
- id3Data = null;
+ 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();
@@ -151,7 +158,7 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public void init(HlsSampleStreamWrapper output) {
extractorOutput = output;
- output.init(uid, previousChunk != null && previousChunk.hlsUrl != hlsUrl);
+ output.init(uid, shouldSpliceIn);
}
@Override
@@ -191,8 +198,8 @@ import java.util.concurrent.atomic.AtomicInteger;
// Internal loading methods.
private void maybeLoadInitData() throws IOException, InterruptedException {
- if ((previousChunk != null && previousChunk.extractor == extractor) || initLoadCompleted
- || initDataSpec == null) {
+ 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);
@@ -325,9 +332,6 @@ import java.util.concurrent.atomic.AtomicInteger;
private Extractor buildExtractorByExtension() {
// Set the extractor that will read the chunk.
Extractor extractor;
- boolean needNewExtractor = previousChunk == null
- || previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
- || trackFormat != previousChunk.trackFormat;
boolean usingNewExtractor = true;
if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
|| lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
@@ -335,7 +339,7 @@ import java.util.concurrent.atomic.AtomicInteger;
} else if (!needNewExtractor) {
// Only reuse TS and fMP4 extractors.
usingNewExtractor = false;
- extractor = previousChunk.extractor;
+ extractor = previousExtractor;
} else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) {
extractor = new FragmentedMp4Extractor(0, timestampAdjuster);
} else {
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
index 869efa6cdc..10e12f0ec6 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -94,24 +94,33 @@ public final class HlsMediaSource implements MediaSource,
@Override
public void releaseSource() {
- playlistTracker.release();
- playlistTracker = null;
+ if (playlistTracker != null) {
+ playlistTracker.release();
+ playlistTracker = null;
+ }
sourceListener = null;
}
@Override
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
SinglePeriodTimeline timeline;
+ long windowDefaultStartPositionUs = playlist.startOffsetUs;
if (playlistTracker.isLive()) {
- // TODO: fix windowPositionInPeriodUs when playlist is empty.
+ long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs)
+ : C.TIME_UNSET;
List segments = playlist.segments;
- long windowDefaultStartPositionUs = segments.isEmpty() ? 0
- : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;
- timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs,
+ 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, 0, true, false);
+ playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false);
}
sourceListener.onSourceInfoRefreshed(timeline, playlist);
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
index c63cf3e5a4..04fe8a093c 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
@@ -26,7 +26,7 @@ import java.io.IOException;
/* package */ final class HlsSampleStream implements SampleStream {
public final int group;
-
+
private final HlsSampleStreamWrapper sampleStreamWrapper;
public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int group) {
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
index 4aaec59f7d..b7426fd03d 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
@@ -40,7 +40,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) {
Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null,
- Format.NO_VALUE);
+ Format.NO_VALUE, 0, null);
return new HlsUrl(null, baseUri, format, null, null, null);
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
index fc70ec6de1..0b61b9781e 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -31,7 +31,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final String url;
public final long durationUs;
- public final int discontinuitySequenceNumber;
+ public final int relativeDiscontinuitySequence;
public final long relativeStartTimeUs;
public final boolean isEncrypted;
public final String encryptionKeyUri;
@@ -43,12 +43,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
}
- public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
+ 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.discontinuitySequenceNumber = discontinuitySequenceNumber;
+ this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs;
this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri;
@@ -65,7 +65,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
+ 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;
@@ -75,11 +78,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final List segments;
public final long durationUs;
- public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence,
- int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime,
+ public HlsMediaPlaylist(String baseUri, long startOffsetUs, long startTimeUs,
+ boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version,
+ long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime,
Segment initializationSegment, List segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.startTimeUs = startTimeUs;
+ this.hasDiscontinuitySequence = hasDiscontinuitySequence;
+ this.discontinuitySequence = discontinuitySequence;
this.mediaSequence = mediaSequence;
this.version = version;
this.targetDurationUs = targetDurationUs;
@@ -87,28 +93,68 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
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) {
- return other == null || mediaSequence > other.mediaSequence
- || (mediaSequence == other.mediaSequence && segments.size() > other.segments.size())
- || (hasEndTag && !other.hasEndTag);
+ 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;
}
- public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) {
- return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, targetDurationUs,
- hasEndTag, hasProgramDateTime, initializationSegment, segments);
+ /**
+ * 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(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(baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence,
+ discontinuitySequence, mediaSequence, version, targetDurationUs, true, hasProgramDateTime,
+ initializationSegment, segments);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
index 1932caccf7..c349bbee05 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -39,16 +39,33 @@ import java.util.regex.Pattern;
*/
public final class HlsPlaylistParser implements ParsingLoadable.Parser {
+ /**
+ * Thrown if the input does not start with an HLS playlist header.
+ */
+ public static final class UnrecognizedInputFormatException extends ParserException {
+
+ public final Uri inputUri;
+
+ public UnrecognizedInputFormatException(Uri inputUri) {
+ super("Input does not start with the #EXTM3U header. Uri: " + inputUri);
+ this.inputUri = inputUri;
+ }
+
+ }
+
+ private static final String PLAYLIST_HEADER = "#EXTM3U";
+
private static final String TAG_VERSION = "#EXT-X-VERSION";
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_TARGET_DURATION = "#EXT-X-TARGETDURATION";
+ 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";
@@ -74,6 +91,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser extraLines = new LinkedList<>();
String line;
try {
+ if (!checkPlaylistHeader(reader)) {
+ throw new UnrecognizedInputFormatException(uri);
+ }
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) {
@@ -119,11 +140,40 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = new ArrayList<>();
@@ -190,7 +240,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>();
long segmentDurationUs = 0;
- int discontinuitySequenceNumber = 0;
+ boolean hasDiscontinuitySequence = false;
+ int playlistDiscontinuitySequence = 0;
+ int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
@@ -229,7 +282,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser oldSegments = oldPlaylist.segments;
- int oldPlaylistSize = oldSegments.size();
- if (!newPlaylist.isNewerThan(oldPlaylist)) {
- // Playlist has not changed.
- return oldPlaylist;
- }
- int mediaSequenceOffset = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
- if (mediaSequenceOffset <= oldPlaylistSize) {
- long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize
- ? oldPlaylist.getEndTimeUs()
- : oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs;
- return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs);
- }
- // No segments overlap, we assume the new playlist start coincides with the primary playlist.
- return newPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
+ return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}
/**
@@ -460,15 +486,15 @@ public final class HlsPlaylistTracker implements Loader.Callback {
List codecSpecificData = buildCodecSpecificData(
parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));
format = Format.createVideoContainerFormat(id, MimeTypes.VIDEO_MP4, sampleMimeType, null,
- bitrate, width, height, Format.NO_VALUE, codecSpecificData);
+ bitrate, width, height, Format.NO_VALUE, codecSpecificData, 0);
} else if (type == C.TRACK_TYPE_AUDIO) {
sampleMimeType = sampleMimeType == null ? MimeTypes.AUDIO_AAC : sampleMimeType;
int channels = parseRequiredInt(parser, KEY_CHANNELS);
@@ -644,8 +644,8 @@ public class SsManifestParser implements ParsingLoadable.Parser {
format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType,
null, bitrate, 0, language);
} else {
- format = Format.createContainerFormat(id, MimeTypes.APPLICATION_MP4, null, sampleMimeType,
- bitrate);
+ format = Format.createContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType, null,
+ bitrate, 0, null);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
index 8a8a37d5ed..ee8a430a71 100644
--- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
+++ b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
@@ -67,6 +67,7 @@ public interface SubtitleDecoderFactory {
* AAS/SSA ({@link SSADecoder})
* TX3G ({@link Tx3gDecoder})
* Cea608 ({@link Cea608Decoder})
+ * Cea708 ({@link Cea708Decoder})
*
*/
SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() {
@@ -83,6 +84,7 @@ public interface SubtitleDecoderFactory {
if (clazz == null) {
throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
}
+<<<<<<< HEAD
if(clazz == SSADecoder.class) {
byte[] header = format.initializationData.get(1);
String dlgfmt = new String(format.initializationData.get(0), "UTF-8");
@@ -94,6 +96,16 @@ public interface SubtitleDecoderFactory {
.newInstance(format.sampleMimeType, format.accessibilityChannel);
}
else {
+=======
+ if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA608)
+ || format.sampleMimeType.equals(MimeTypes.APPLICATION_MP4CEA608)) {
+ return clazz.asSubclass(SubtitleDecoder.class).getConstructor(String.class, Integer.TYPE)
+ .newInstance(format.sampleMimeType, format.accessibilityChannel);
+ } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) {
+ return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE)
+ .newInstance(format.accessibilityChannel);
+ } else {
+>>>>>>> upstream/dev-v2
return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance();
}
} catch (Exception e) {
@@ -122,6 +134,8 @@ public interface SubtitleDecoderFactory {
case MimeTypes.APPLICATION_CEA608:
case MimeTypes.APPLICATION_MP4CEA608:
return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder");
+ case MimeTypes.APPLICATION_CEA708:
+ return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder");
default:
return null;
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java
new file mode 100644
index 0000000000..e63d1d4118
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.text.Layout.Alignment;
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A {@link Cue} for CEA-708.
+ */
+/* package */ final class Cea708Cue extends Cue implements Comparable {
+
+ /**
+ * 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(Cea708Cue other) {
+ if (other.priority < priority) {
+ return -1;
+ } else if (other.priority > priority) {
+ return 1;
+ }
+ return 0;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
new file mode 100644
index 0000000000..5ca5ce1270
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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").
+ *
+ * This implementation does not provide full compatibility with the CEA-708 specification. Note
+ * that only the default pen/text and window/cue colors (i.e. text with
+ * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK}
+ * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with
+ * device accessibility settings; all others will use the colors and opacity specified by the
+ * caption data.
+ */
+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 cues;
+ private List 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) {
+ finalizeCurrentPacket();
+ 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 + ")");
+ }
+
+ 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;
+ }
+
+ while (serviceBlockPacket.bitsLeft() > 0) {
+ int command = serviceBlockPacket.readBits(8);
+ if (command != COMMAND_EXT1) {
+ if (command <= GROUP_C0_END) {
+ handleC0Command(command);
+ } else if (command <= GROUP_G0_END) {
+ handleG0Character(command);
+ } else if (command <= GROUP_C1_END) {
+ handleC1Command(command);
+ // Cues are always updated after a C1 command
+ cues = getDisplayCues();
+ } else if (command <= GROUP_G1_END) {
+ handleG1Character(command);
+ } 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);
+ } else if (command <= GROUP_C3_END) {
+ handleC3Command(command);
+ } else if (command <= GROUP_G3_END) {
+ handleG3Character(command);
+ } else {
+ Log.w(TAG, "Invalid extended command: " + command);
+ }
+ }
+ }
+ }
+
+ 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);
+ 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 <= 0x0F) {
+ // 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 getDisplayCues() {
+ List 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.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 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;
+
+ 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;
+ }
+
+ 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.
+ }
+
+ 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));
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
index 77df9a2173..690723cf15 100644
--- a/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
+++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -15,12 +15,13 @@
*/
package com.google.android.exoplayer2.trackselection;
-import android.util.Pair;
+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;
@@ -82,12 +83,14 @@ public abstract class MappingTrackSelector extends TrackSelector {
private final SparseArray
+ * {@code default_artwork} - Default artwork to use if no artwork available in audio
+ * streams.
+ *
+ * - Corresponding method: {@link #setDefaultArtwork(Bitmap)}
+ * - Default: {@code null}
+ *
+ *
* {@code use_controller} - Whether playback controls are displayed.
*
* - Corresponding method: {@link #setUseController(boolean)}
@@ -179,6 +186,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
private SimpleExoPlayer player;
private boolean useController;
private boolean useArtwork;
+ private Bitmap defaultArtwork;
private int controllerShowTimeoutMs;
public SimpleExoPlayerView(Context context) {
@@ -194,6 +202,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
int playerLayoutId = R.layout.exo_simple_player_view;
boolean useArtwork = true;
+ int defaultArtworkId = 0;
boolean useController = true;
int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
@@ -205,6 +214,8 @@ public final class SimpleExoPlayerView extends FrameLayout {
playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id,
playerLayoutId);
useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork);
+ defaultArtworkId = a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork,
+ defaultArtworkId);
useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController);
surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType);
resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode);
@@ -246,6 +257,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
// Artwork view.
artworkView = (ImageView) findViewById(R.id.exo_artwork);
this.useArtwork = useArtwork && artworkView != null;
+ if (defaultArtworkId != 0) {
+ defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId);
+ }
// Subtitle view.
subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles);
@@ -351,6 +365,26 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
}
+ /**
+ * Returns the default artwork to display.
+ */
+ public Bitmap getDefaultArtwork() {
+ return defaultArtwork;
+ }
+
+ /**
+ * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is
+ * present in the media.
+ *
+ * @param defaultArtwork the default artwork to display.
+ */
+ public void setDefaultArtwork(Bitmap defaultArtwork) {
+ if (this.defaultArtwork != defaultArtwork) {
+ this.defaultArtwork = defaultArtwork;
+ updateForCurrentTrackSelections();
+ }
+ }
+
/**
* Returns whether the playback controls are enabled.
*/
@@ -569,6 +603,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
}
}
}
+ if (setArtworkFromBitmap(defaultArtwork)) {
+ return;
+ }
}
// Artwork disabled or unavailable.
hideArtwork();
@@ -580,18 +617,23 @@ public final class SimpleExoPlayerView extends FrameLayout {
if (metadataEntry instanceof ApicFrame) {
byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData;
Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
- if (bitmap != null) {
- int bitmapWidth = bitmap.getWidth();
- int bitmapHeight = bitmap.getHeight();
- if (bitmapWidth > 0 && bitmapHeight > 0) {
- if (contentFrame != null) {
- contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight);
- }
- artworkView.setImageBitmap(bitmap);
- artworkView.setVisibility(VISIBLE);
- return true;
- }
+ return setArtworkFromBitmap(bitmap);
+ }
+ }
+ return false;
+ }
+
+ private boolean setArtworkFromBitmap(Bitmap bitmap) {
+ if (bitmap != null) {
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getHeight();
+ if (bitmapWidth > 0 && bitmapHeight > 0) {
+ if (contentFrame != null) {
+ contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight);
}
+ artworkView.setImageBitmap(bitmap);
+ artworkView.setVisibility(VISIBLE);
+ return true;
}
}
return false;
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
index 0ddf17cbe9..4a2354e180 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
@@ -65,7 +65,7 @@ public interface DataSource {
* @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 avaliable
+ * @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.
*/
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
index d251446976..133e71f6e2 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -32,7 +32,7 @@ public final class DataSpec {
* The flags that apply to any request for data.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {FLAG_ALLOW_GZIP})
+ @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.
@@ -45,7 +45,10 @@ public final class DataSpec {
* {@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;
+ 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.
@@ -76,7 +79,8 @@ public final class DataSpec {
*/
public final String key;
/**
- * Request flags. Currently {@link #FLAG_ALLOW_GZIP} is the only supported flag.
+ * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
+ * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
*/
@Flags
public final int flags;
@@ -167,6 +171,15 @@ public final class DataSpec {
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
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
index b326c41b18..ca0fda9399 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -230,7 +230,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
- if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
+ if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
@@ -343,7 +343,7 @@ public class DefaultHttpDataSource implements HttpDataSource {
byte[] postBody = dataSpec.postBody;
long position = dataSpec.position;
long length = dataSpec.length;
- boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
+ boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
if (!allowCrossProtocolRedirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
index dcfed59145..615eb4df97 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
@@ -15,10 +15,11 @@
*/
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 implements Factory {
+public final class DefaultHttpDataSourceFactory extends BaseFactory {
private final String userAgent;
private final TransferListener super DataSource> listener;
@@ -75,8 +76,9 @@ public final class DefaultHttpDataSourceFactory implements Factory {
}
@Override
- public DefaultHttpDataSource createDataSource() {
+ protected DefaultHttpDataSource createDataSourceInternal() {
return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis,
readTimeoutMillis, allowCrossProtocolRedirects);
}
+
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
index f915ee4e24..8df8624102 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -17,11 +17,13 @@ package com.google.android.exoplayer2.upstream;
import android.support.annotation.IntDef;
import android.text.TextUtils;
+import com.google.android.exoplayer2.util.Assertions;
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.HashMap;
import java.util.List;
import java.util.Map;
@@ -38,6 +40,86 @@ public interface HttpDataSource extends DataSource {
@Override
HttpDataSource createDataSource();
+ /**
+ * Sets a default request header field for {@link HttpDataSource} instances subsequently
+ * created by the factory. Previously created instances are not affected.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ void setDefaultRequestProperty(String name, String value);
+
+ /**
+ * Clears a default request header field for {@link HttpDataSource} instances subsequently
+ * created by the factory. Previously created instances are not affected.
+ *
+ * @param name The name of the header field.
+ */
+ void clearDefaultRequestProperty(String name);
+
+ /**
+ * Clears all default request header fields for all {@link HttpDataSource} instances
+ * subsequently created by the factory. Previously created instances are not affected.
+ */
+ void clearAllDefaultRequestProperties();
+
+ }
+
+ /**
+ * Base implementation of {@link Factory} that sets default request properties.
+ */
+ abstract class BaseFactory implements Factory {
+
+ private final HashMap requestProperties;
+
+ public BaseFactory() {
+ requestProperties = new HashMap<>();
+ }
+
+ @Override
+ public final HttpDataSource createDataSource() {
+ HttpDataSource dataSource = createDataSourceInternal();
+ synchronized (requestProperties) {
+ for (Map.Entry property : requestProperties.entrySet()) {
+ dataSource.setRequestProperty(property.getKey(), property.getValue());
+ }
+ }
+ return dataSource;
+ }
+
+ @Override
+ public final void setDefaultRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ synchronized (requestProperties) {
+ requestProperties.put(name, value);
+ }
+ }
+
+ @Override
+ public final void clearDefaultRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ synchronized (requestProperties) {
+ requestProperties.remove(name);
+ }
+ }
+
+ @Override
+ public final void clearAllDefaultRequestProperties() {
+ synchronized (requestProperties) {
+ requestProperties.clear();
+ }
+ }
+
+ /**
+ * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance without
+ * default request properties set. Default request properties will be set by
+ * {@link #createDataSource()} before the instance is returned.
+ *
+ * @return A {@link HttpDataSource} instance without default request properties set.
+ */
+ protected abstract HttpDataSource createDataSourceInternal();
+
}
/**
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
index c23b609704..c25638ac86 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java
@@ -19,6 +19,7 @@ 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;
@@ -114,7 +115,7 @@ public final class ParsingLoadable implements Loadable {
result = parser.parse(dataSource.getUri(), inputStream);
} finally {
bytesLoaded = inputStream.bytesRead();
- inputStream.close();
+ Util.closeQuietly(inputStream);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
index d57f3ee140..71397bd403 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -81,10 +81,12 @@ public final class CacheDataSink implements DataSink {
@Override
public void open(DataSpec dataSpec) throws CacheDataSinkException {
- this.dataSpec = dataSpec;
- if (dataSpec.length == C.LENGTH_UNSET) {
+ 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();
@@ -95,7 +97,7 @@ public final class CacheDataSink implements DataSink {
@Override
public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
- if (dataSpec.length == C.LENGTH_UNSET) {
+ if (dataSpec == null) {
return;
}
try {
@@ -119,7 +121,7 @@ public final class CacheDataSink implements DataSink {
@Override
public void close() throws CacheDataSinkException {
- if (dataSpec == null || dataSpec.length == C.LENGTH_UNSET) {
+ if (dataSpec == null) {
return;
}
try {
@@ -130,8 +132,10 @@ public final class CacheDataSink implements DataSink {
}
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,
- Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize));
+ maxLength);
underlyingFileOutputStream = new FileOutputStream(file);
if (bufferSize > 0) {
if (bufferedOutputStream == null) {
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
index 9e38dabc31..58cc70d68d 100644
--- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -67,14 +67,25 @@ import javax.crypto.spec.SecretKeySpec;
private boolean changed;
private ReusableBufferedOutputStream bufferedOutputStream;
- /** Creates a CachedContentIndex which works on the index file in the given cacheDir. */
+ /**
+ * 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. */
+ /**
+ * 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");
@@ -302,6 +313,9 @@ import javax.crypto.spec.SecretKeySpec;
}
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 {
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
new file mode 100644
index 0000000000..0f08ca40f2
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.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.upstream.cache;
+
+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 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 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 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 {
+
+ /**
+ * 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(Region another) {
+ return startOffset < another.startOffset ? -1
+ : startOffset == another.startOffset ? 0 : 1;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
new file mode 100644
index 0000000000..ccf9a5b3f5
--- /dev/null
+++ b/library/src/main/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();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
new file mode 100644
index 0000000000..26ac3b38fa
--- /dev/null
+++ b/library/src/main/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();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
new file mode 100644
index 0000000000..e093eb3064
--- /dev/null
+++ b/library/src/main/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();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
new file mode 100644
index 0000000000..ff8841fa9c
--- /dev/null
+++ b/library/src/main/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;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
index 1f148a5fa6..1eb4300142 100644
--- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
+++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java
@@ -66,12 +66,12 @@ public final class MimeTypes {
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_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
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";
@@ -80,6 +80,7 @@ public final class MimeTypes {
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";
private MimeTypes() {}
@@ -217,12 +218,16 @@ public final class MimeTypes {
} else if (isVideo(mimeType)) {
return C.TRACK_TYPE_VIDEO;
} else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType)
- || APPLICATION_CEA708.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_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)) {
return C.TRACK_TYPE_TEXT;
- } else if (APPLICATION_ID3.equals(mimeType)) {
+ } 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;
diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java
index b8d635a053..ef4aa05cfe 100644
--- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java
+++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java
@@ -423,27 +423,6 @@ public final class ParsableByteArray {
return readString(length, Charset.defaultCharset());
}
- /**
- * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is ignored,
- * if present.
- *
- * @param length The number of bytes to read.
- * @return The string encoded by the bytes.
- */
- 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, Charset.defaultCharset());
- position += length;
- return result;
- }
-
/**
* Reads the next {@code length} bytes as characters in the specified {@link Charset}.
*
@@ -457,6 +436,49 @@ public final class ParsableByteArray {
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.
*
@@ -464,15 +486,15 @@ public final class ParsableByteArray {
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default
* charset (UTF-8) is used.
*
- * @return A String containing the contents of the line, not including any line-termination
- * characters, or null if the end of the stream has been reached.
+ * @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 && data[lineLimit] != '\n' && data[lineLimit] != '\r') {
+ while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {
lineLimit++;
}
if (lineLimit - position >= 3 && data[position] == (byte) 0xEF
diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java b/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
index a3d1d4d02e..1db3d2c1f4 100644
--- a/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
+++ b/library/src/main/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
@@ -67,6 +67,7 @@ public final class ReusableBufferedOutputStream extends BufferedOutputStream {
public void reset(OutputStream out) {
Assertions.checkState(closed);
this.out = out;
+ count = 0;
closed = false;
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java b/library/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java
index 8b1af1f0c8..c43b1929cb 100644
--- a/library/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java
+++ b/library/src/main/java/com/google/android/exoplayer2/util/SlidingPercentile.java
@@ -32,7 +32,7 @@ import java.util.Comparator;
* @see Wiki: Moving average
* @see Wiki: Selection algorithm
*/
-public final class SlidingPercentile {
+public class SlidingPercentile {
// Orderings.
private static final Comparator INDEX_COMPARATOR = new Comparator() {
diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java
index 4477de7abb..e854c05165 100644
--- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -254,6 +254,16 @@ public final class Util {
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}.
*
diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index f68b72fb65..62224a64d6 100644
--- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -19,6 +19,7 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.MediaCodec;
+import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Handler;
@@ -28,6 +29,7 @@ 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;
@@ -83,6 +85,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private int lastReportedUnappliedRotationDegrees;
private float lastReportedPixelWidthHeightRatio;
+ private boolean tunneling;
+ private int tunnelingAudioSessionId;
+ /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
+
/**
* @param context A context.
* @param mediaCodecSelector A decoder selector.
@@ -172,7 +178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType,
- requiresSecureDecryption, false);
+ requiresSecureDecryption);
if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE;
}
@@ -196,13 +202,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
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 | formatSupport;
+ 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();
}
@@ -216,7 +225,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);
- renderedFirstFrame = false;
+ clearRenderedFirstFrame();
consecutiveDroppedFrameCount = 0;
joiningDeadlineMs = joining && allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
@@ -263,6 +272,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
pendingPixelWidthHeightRatio = Format.NO_VALUE;
clearLastReportedVideoSize();
frameReleaseTimeHelper.disable();
+ tunnelingOnFrameRenderedListener = null;
try {
super.onDisabled();
} finally {
@@ -287,11 +297,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
private void setSurface(Surface surface) throws ExoPlaybackException {
- // Clear state so that we always call the event listener with the video size and when a frame
- // is rendered, even if the surface hasn't changed.
- renderedFirstFrame = false;
- clearLastReportedVideoSize();
- // We only need to actually release and reinitialize the codec if the surface has changed.
+ // We only need to release and reinitialize the codec if the surface has changed.
if (this.surface != surface) {
this.surface = surface;
int state = getState();
@@ -300,6 +306,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeInitCodec();
}
}
+ // Clear state so that we always call the event listener with the video size and when a frame
+ // is rendered, even if the surface hasn't changed.
+ clearRenderedFirstFrame();
+ clearLastReportedVideoSize();
}
@Override
@@ -310,8 +320,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) {
codecMaxValues = getCodecMaxValues(format, streamFormats);
- MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround);
+ MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
+ tunnelingAudioSessionId);
codec.configure(mediaFormat, surface, crypto, 0);
+ if (Util.SDK_INT >= 23 && tunneling) {
+ tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
+ }
}
@Override
@@ -328,6 +342,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
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)
@@ -438,10 +459,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
- * Returns true if the current frame should be dropped.
+ * Returns whether the buffer being processed should be dropped.
*
- * @param earlyUs Time indicating how early the frame is. Negative values indicate late frame.
- * @param elapsedRealtimeUs Wall clock time.
+ * @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.
@@ -476,10 +499,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
TraceUtil.endSection();
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
- if (!renderedFirstFrame) {
- renderedFirstFrame = true;
- eventDispatcher.renderedFirstFrame(surface);
- }
+ maybeNotifyRenderedFirstFrame();
}
@TargetApi(21)
@@ -490,6 +510,25 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
TraceUtil.endSection();
decoderCounters.renderedOutputBufferCount++;
consecutiveDroppedFrameCount = 0;
+ maybeNotifyRenderedFirstFrame();
+ }
+
+ 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);
@@ -528,7 +567,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@SuppressLint("InlinedApi")
private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues,
- boolean deviceNeedsAutoFrcWorkaround) {
+ boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) {
MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16();
// Set the maximum adaptive video dimensions.
frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width);
@@ -541,9 +580,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (deviceNeedsAutoFrcWorkaround) {
frameworkMediaFormat.setInteger("auto-frc", 0);
}
+ // Configure tunneling if enabled.
+ if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId);
+ }
return frameworkMediaFormat;
}
+ @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}.
@@ -679,4 +728,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
+ @TargetApi(23)
+ private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener {
+
+ private OnFrameRenderedListenerV23(MediaCodec codec) {
+ codec.setOnFrameRenderedListener(this, new Handler());
+ }
+
+ @Override
+ public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) {
+ if (this != tunnelingOnFrameRenderedListener) {
+ // Stale event.
+ return;
+ }
+ maybeNotifyRenderedFirstFrame();
+ }
+
+ }
+
}
diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml b/library/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml
new file mode 100644
index 0000000000..4b86e109e9
--- /dev/null
+++ b/library/src/main/res/drawable-anydpi-v21/exo_controls_fastforward.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_next.xml b/library/src/main/res/drawable-anydpi-v21/exo_controls_next.xml
new file mode 100644
index 0000000000..6305bcbc90
--- /dev/null
+++ b/library/src/main/res/drawable-anydpi-v21/exo_controls_next.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml b/library/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml
new file mode 100644
index 0000000000..45cd68bed6
--- /dev/null
+++ b/library/src/main/res/drawable-anydpi-v21/exo_controls_pause.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_play.xml b/library/src/main/res/drawable-anydpi-v21/exo_controls_play.xml
new file mode 100644
index 0000000000..c8c4cdb127
--- /dev/null
+++ b/library/src/main/res/drawable-anydpi-v21/exo_controls_play.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml b/library/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml
new file mode 100644
index 0000000000..9564a2a350
--- /dev/null
+++ b/library/src/main/res/drawable-anydpi-v21/exo_controls_previous.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml b/library/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml
new file mode 100644
index 0000000000..976b706170
--- /dev/null
+++ b/library/src/main/res/drawable-anydpi-v21/exo_controls_rewind.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
index c65956ab7f..843df84091 100644
Binary files a/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png differ
diff --git a/library/src/main/res/drawable-hdpi/exo_controls_next.png b/library/src/main/res/drawable-hdpi/exo_controls_next.png
index 6e27b8161e..c37541472e 100644
Binary files a/library/src/main/res/drawable-hdpi/exo_controls_next.png and b/library/src/main/res/drawable-hdpi/exo_controls_next.png differ
diff --git a/library/src/main/res/drawable-hdpi/exo_controls_pause.png b/library/src/main/res/drawable-hdpi/exo_controls_pause.png
index 1d465a41e4..0a23452746 100644
Binary files a/library/src/main/res/drawable-hdpi/exo_controls_pause.png and b/library/src/main/res/drawable-hdpi/exo_controls_pause.png differ
diff --git a/library/src/main/res/drawable-hdpi/exo_controls_play.png b/library/src/main/res/drawable-hdpi/exo_controls_play.png
index 2746d17fb1..e98e2b9cbe 100644
Binary files a/library/src/main/res/drawable-hdpi/exo_controls_play.png and b/library/src/main/res/drawable-hdpi/exo_controls_play.png differ
diff --git a/library/src/main/res/drawable-hdpi/exo_controls_previous.png b/library/src/main/res/drawable-hdpi/exo_controls_previous.png
index 85b3766904..3eae5c883b 100644
Binary files a/library/src/main/res/drawable-hdpi/exo_controls_previous.png and b/library/src/main/res/drawable-hdpi/exo_controls_previous.png differ
diff --git a/library/src/main/res/drawable-hdpi/exo_controls_rewind.png b/library/src/main/res/drawable-hdpi/exo_controls_rewind.png
index a4ac181777..36537d3b73 100644
Binary files a/library/src/main/res/drawable-hdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-hdpi/exo_controls_rewind.png differ
diff --git a/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png b/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
index 1b4d9dbef9..19b9e6015c 100644
Binary files a/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png differ
diff --git a/library/src/main/res/drawable-ldpi/exo_controls_next.png b/library/src/main/res/drawable-ldpi/exo_controls_next.png
index 99927fd27b..d4872037aa 100644
Binary files a/library/src/main/res/drawable-ldpi/exo_controls_next.png and b/library/src/main/res/drawable-ldpi/exo_controls_next.png differ
diff --git a/library/src/main/res/drawable-ldpi/exo_controls_pause.png b/library/src/main/res/drawable-ldpi/exo_controls_pause.png
index 3b98d66688..616ec42f39 100644
Binary files a/library/src/main/res/drawable-ldpi/exo_controls_pause.png and b/library/src/main/res/drawable-ldpi/exo_controls_pause.png differ
diff --git a/library/src/main/res/drawable-ldpi/exo_controls_play.png b/library/src/main/res/drawable-ldpi/exo_controls_play.png
index e7c19724bb..5d1c702892 100644
Binary files a/library/src/main/res/drawable-ldpi/exo_controls_play.png and b/library/src/main/res/drawable-ldpi/exo_controls_play.png differ
diff --git a/library/src/main/res/drawable-ldpi/exo_controls_previous.png b/library/src/main/res/drawable-ldpi/exo_controls_previous.png
index df043228d0..930534d312 100644
Binary files a/library/src/main/res/drawable-ldpi/exo_controls_previous.png and b/library/src/main/res/drawable-ldpi/exo_controls_previous.png differ
diff --git a/library/src/main/res/drawable-ldpi/exo_controls_rewind.png b/library/src/main/res/drawable-ldpi/exo_controls_rewind.png
index 28843f9fb0..83d71782f6 100644
Binary files a/library/src/main/res/drawable-ldpi/exo_controls_rewind.png and b/library/src/main/res/drawable-ldpi/exo_controls_rewind.png differ
diff --git a/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
index 170dd2daaa..ee3efe1d69 100644
Binary files a/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png differ
diff --git a/library/src/main/res/drawable-mdpi/exo_controls_next.png b/library/src/main/res/drawable-mdpi/exo_controls_next.png
index fcd73d90e7..9d4d7469ed 100644
Binary files a/library/src/main/res/drawable-mdpi/exo_controls_next.png and b/library/src/main/res/drawable-mdpi/exo_controls_next.png differ
diff --git a/library/src/main/res/drawable-mdpi/exo_controls_pause.png b/library/src/main/res/drawable-mdpi/exo_controls_pause.png
index 3e6b2a17b5..f54c942201 100644
Binary files a/library/src/main/res/drawable-mdpi/exo_controls_pause.png and b/library/src/main/res/drawable-mdpi/exo_controls_pause.png differ
diff --git a/library/src/main/res/drawable-mdpi/exo_controls_play.png b/library/src/main/res/drawable-mdpi/exo_controls_play.png
index 7966bbc516..dd0c142859 100644
Binary files a/library/src/main/res/drawable-mdpi/exo_controls_play.png and b/library/src/main/res/drawable-mdpi/exo_controls_play.png differ
diff --git a/library/src/main/res/drawable-mdpi/exo_controls_previous.png b/library/src/main/res/drawable-mdpi/exo_controls_previous.png
index b653d05b9f..950e213d2f 100644
Binary files a/library/src/main/res/drawable-mdpi/exo_controls_previous.png and b/library/src/main/res/drawable-mdpi/exo_controls_previous.png differ
diff --git a/library/src/main/res/drawable-mdpi/exo_controls_rewind.png b/library/src/main/res/drawable-mdpi/exo_controls_rewind.png
index 5489180eb1..e75efae189 100644
Binary files a/library/src/main/res/drawable-mdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-mdpi/exo_controls_rewind.png differ
diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
index 60f7e92181..ead712cfe9 100644
Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png differ
diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_next.png b/library/src/main/res/drawable-xhdpi/exo_controls_next.png
index 4def965cec..bc1ebf83c5 100644
Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_next.png and b/library/src/main/res/drawable-xhdpi/exo_controls_next.png differ
diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_pause.png b/library/src/main/res/drawable-xhdpi/exo_controls_pause.png
index 6bd3d482e1..1c868f1831 100644
Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_pause.png and b/library/src/main/res/drawable-xhdpi/exo_controls_pause.png differ
diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_play.png b/library/src/main/res/drawable-xhdpi/exo_controls_play.png
index ccfef18056..f2f934413e 100644
Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_play.png and b/library/src/main/res/drawable-xhdpi/exo_controls_play.png differ
diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_previous.png b/library/src/main/res/drawable-xhdpi/exo_controls_previous.png
index c4472ae2d9..d197eff873 100644
Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_previous.png and b/library/src/main/res/drawable-xhdpi/exo_controls_previous.png differ
diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png b/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
index 167d10e58b..3340ef9bd2 100644
Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png
index ab9e022fbf..e1c6cae292 100644
Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_next.png b/library/src/main/res/drawable-xxhdpi/exo_controls_next.png
index ce0a14325a..232f09e910 100644
Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_next.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_next.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png b/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
index 9a36b17cb8..50a545db4d 100644
Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_play.png b/library/src/main/res/drawable-xxhdpi/exo_controls_play.png
index 41f76bbf99..08508c5015 100644
Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_play.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_play.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png b/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
index d4688741b9..f71acc4875 100644
Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png b/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
index 8ebb2ccf30..db0555f9e5 100644
Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png differ
diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml
index b5c01b4575..c73bfb0a3c 100644
--- a/library/src/main/res/values/attrs.xml
+++ b/library/src/main/res/values/attrs.xml
@@ -37,6 +37,7 @@
+
diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle
index c099e2c86e..c53793b534 100644
--- a/playbacktests/build.gradle
+++ b/playbacktests/build.gradle
@@ -21,17 +21,6 @@ android {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
- }
- }
-
- lintOptions {
- abortOnError false
- }
}
dependencies {
diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java
index 8c1ee45e0e..6b561bc81c 100644
--- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java
+++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java
@@ -26,13 +26,14 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
-import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
@@ -275,7 +276,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2 buildDrmSessionManager(
+ protected final DefaultDrmSessionManager buildDrmSessionManager(
final String userAgent) {
- StreamingDrmSessionManager drmSessionManager = null;
+ DefaultDrmSessionManager drmSessionManager = null;
if (isWidevineEncrypted) {
try {
// Force L3 if secure decoder is not available.
- boolean forceL3Widevine =
- MediaCodecUtil.getDecoderInfo(videoMimeType, true, false) == null;
+ boolean forceL3Widevine = MediaCodecUtil.getDecoderInfo(videoMimeType, true) == null;
MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID);
String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY);
String widevineContentId = forceL3Widevine ? WIDEVINE_SW_CRYPTO_CONTENT_ID
@@ -716,7 +716,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2 trackIndices = new ArrayList<>();
// Always select explicitly listed representations.
for (String formatId : formatIds) {
- trackIndices.add(getTrackIndex(trackGroup, formatId));
+ int trackIndex = getTrackIndex(trackGroup, formatId);
+ Log.d(TAG, "Adding base video format: "
+ + Format.toLogString(trackGroup.getFormat(trackIndex)));
+ trackIndices.add(trackIndex);
}
// Select additional video representations, if supported by the device.
if (canIncludeAdditionalFormats) {
for (int i = 0; i < trackGroup.length; i++) {
if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) {
- Log.d(TAG, "Adding video format: " + trackGroup.getFormat(i).id);
+ Log.d(TAG, "Adding extra video format: "
+ + Format.toLogString(trackGroup.getFormat(i)));
trackIndices.add(i);
}
}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java
index e279bc8ae8..ede172ad29 100644
--- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java
+++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugSimpleExoPlayer.java
@@ -19,6 +19,7 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.os.Handler;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.SimpleExoPlayer;
@@ -66,16 +67,14 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer {
private int startIndex;
private int queueSize;
private int bufferCount;
+ private int minimumInsertIndex;
public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
long allowedJoiningTimeMs, Handler eventHandler,
DrmSessionManager drmSessionManager,
- VideoRendererEventListener eventListener,
- int maxDroppedFrameCountToNotify) {
+ VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) {
super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, false,
eventHandler, eventListener, maxDroppedFrameCountToNotify);
- startIndex = 0;
- queueSize = 0;
}
@Override
@@ -90,6 +89,14 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer {
clearTimestamps();
}
+ @Override
+ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ super.onInputFormatChanged(newFormat);
+ // Ensure timestamps of buffers queued after this format change are never inserted into the
+ // queue of expected output timestamps before those of buffers that have already been queued.
+ minimumInsertIndex = startIndex + queueSize;
+ }
+
@Override
protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
insertTimestamp(buffer.timeUs);
@@ -111,10 +118,11 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer {
startIndex = 0;
queueSize = 0;
bufferCount = 0;
+ minimumInsertIndex = 0;
}
private void insertTimestamp(long presentationTimeUs) {
- for (int i = startIndex + queueSize - 1; i >= startIndex; i--) {
+ for (int i = startIndex + queueSize - 1; i >= minimumInsertIndex; i--) {
if (presentationTimeUs >= timestampsList[i]) {
timestampsList[i + 1] = presentationTimeUs;
queueSize++;
@@ -122,20 +130,22 @@ public class DebugSimpleExoPlayer extends SimpleExoPlayer {
}
timestampsList[i + 1] = timestampsList[i];
}
- timestampsList[startIndex] = presentationTimeUs;
+ timestampsList[minimumInsertIndex] = presentationTimeUs;
queueSize++;
}
private void maybeShiftTimestampsList() {
if (startIndex + queueSize == ARRAY_SIZE) {
System.arraycopy(timestampsList, startIndex, timestampsList, 0, queueSize);
+ minimumInsertIndex -= startIndex;
startIndex = 0;
}
}
private long dequeueTimestamp() {
- startIndex++;
queueSize--;
+ startIndex++;
+ minimumInsertIndex = Math.max(minimumInsertIndex, startIndex);
return timestampsList[startIndex - 1];
}
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
index dfecdd236a..7bf8985b64 100644
--- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
+++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
@@ -247,7 +247,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
@Override
public void onAudioInputFormatChanged(Format format) {
- Log.d(tag, "audioFormatChanged [" + format.id + "]");
+ Log.d(tag, "audioFormatChanged [" + Format.toLogString(format) + "]");
}
@Override
@@ -277,7 +277,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
@Override
public void onVideoInputFormatChanged(Format format) {
- Log.d(tag, "videoFormatChanged [" + format.id + "]");
+ Log.d(tag, "videoFormatChanged [" + Format.toLogString(format) + "]");
}
@Override
diff --git a/testutils/build.gradle b/testutils/build.gradle
index 61bb50f74d..83ff065f9a 100644
--- a/testutils/build.gradle
+++ b/testutils/build.gradle
@@ -1,3 +1,16 @@
+// 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.
apply plugin: 'com.android.library'
android {
@@ -8,13 +21,6 @@ android {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
}
dependencies {