sampleTimes = expectedTrackOutput.getSampleTimesUs();
+ for (int i = 0; i < sampleTimes.size() - 1; i++) {
+ long currentSampleTime = sampleTimes.get(i);
+ long nextSampleTime = sampleTimes.get(i + 1);
+ if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {
+ return i;
+ }
+ }
+ return sampleTimes.size() - 1;
+ }
+
+ private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {
+ DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null);
+ dataSource.open(dataSpec);
+ return new DefaultExtractorInput(dataSource, position, totalInputLength);
+ }
+
+ private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)
+ throws IOException, InterruptedException {
+ byte[] data = TestUtil.getByteArray(context, fileName);
+
+ FlacExtractor extractor = new FlacExtractor();
+ extractor.init(expectedOutput);
+ FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
+
+ while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {}
+ }
+}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
new file mode 100644
index 0000000000..0bbee1ea30
--- /dev/null
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.flac;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * A {@link SeekMap} implementation for FLAC stream using binary search.
+ *
+ * This seeker performs seeking by using binary search within the stream, until it finds the
+ * frame that contains the target sample.
+ */
+/* package */ final class FlacBinarySearchSeeker {
+
+ /**
+ * When seeking within the source, if the offset is smaller than or equal to this value, the seek
+ * operation will be performed using a skip operation. Otherwise, the source will be reloaded at
+ * the new seek position.
+ */
+ private static final long MAX_SKIP_BYTES = 256 * 1024;
+
+ private final FlacStreamInfo streamInfo;
+ private final FlacBinarySearchSeekMap seekMap;
+ private final FlacDecoderJni decoderJni;
+
+ private final long firstFramePosition;
+ private final long inputLength;
+ private final long approxBytesPerFrame;
+
+ private @Nullable SeekOperationParams pendingSeekOperationParams;
+
+ public FlacBinarySearchSeeker(
+ FlacStreamInfo streamInfo,
+ long firstFramePosition,
+ long inputLength,
+ FlacDecoderJni decoderJni) {
+ this.streamInfo = Assertions.checkNotNull(streamInfo);
+ this.decoderJni = Assertions.checkNotNull(decoderJni);
+ this.firstFramePosition = firstFramePosition;
+ this.inputLength = inputLength;
+ this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
+
+ pendingSeekOperationParams = null;
+ seekMap =
+ new FlacBinarySearchSeekMap(
+ streamInfo,
+ firstFramePosition,
+ inputLength,
+ streamInfo.durationUs(),
+ approxBytesPerFrame);
+ }
+
+ /** Returns the seek map for the wrapped FLAC stream. */
+ public SeekMap getSeekMap() {
+ return seekMap;
+ }
+
+ /** Sets the target time in microseconds within the stream to seek to. */
+ public void setSeekTargetUs(long timeUs) {
+ if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
+ return;
+ }
+
+ pendingSeekOperationParams =
+ new SeekOperationParams(
+ timeUs,
+ streamInfo.getSampleIndex(timeUs),
+ /* floorSample= */ 0,
+ /* ceilingSample= */ streamInfo.totalSamples,
+ /* floorPosition= */ firstFramePosition,
+ /* ceilingPosition= */ inputLength,
+ approxBytesPerFrame);
+ }
+
+ /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
+ public boolean hasPendingSeek() {
+ return pendingSeekOperationParams != null;
+ }
+
+ /**
+ * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
+ * {@link Extractor}.
+ *
+ * @param input The {@link ExtractorInput} from which data should be read.
+ * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
+ * to hold the position of the required seek.
+ * @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
+ * updated to hold the extracted frame that contains the target sample. The caller needs to
+ * check the byte buffer limit to see if an extracted frame is available.
+ * @return One of the {@code RESULT_} values defined in {@link Extractor}.
+ * @throws IOException If an error occurred reading from the input.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public int handlePendingSeek(
+ ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
+ throws InterruptedException, IOException {
+ outputBuffer.position(0);
+ outputBuffer.limit(0);
+ while (true) {
+ long floorPosition = pendingSeekOperationParams.floorPosition;
+ long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
+ long searchPosition = pendingSeekOperationParams.nextSearchPosition;
+
+ // streamInfo may not contain minFrameSize, in which case this value will be 0.
+ int minFrameSize = Math.max(1, streamInfo.minFrameSize);
+ if (floorPosition + minFrameSize >= ceilingPosition) {
+ // The seeking range is too small for more than 1 frame, so we can just continue from
+ // the floor position.
+ pendingSeekOperationParams = null;
+ decoderJni.reset(floorPosition);
+ return seekToPosition(input, floorPosition, seekPositionHolder);
+ }
+
+ if (!skipInputUntilPosition(input, searchPosition)) {
+ return seekToPosition(input, searchPosition, seekPositionHolder);
+ }
+
+ decoderJni.reset(searchPosition);
+ try {
+ decoderJni.decodeSampleWithBacktrackPosition(
+ outputBuffer, /* retryPosition= */ searchPosition);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ // For some reasons, the extractor can't find a frame mid-stream.
+ // Stop the seeking and let it re-try playing at the last search position.
+ pendingSeekOperationParams = null;
+ throw new IOException("Cannot read frame at position " + searchPosition, e);
+ }
+ if (outputBuffer.limit() == 0) {
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+
+ long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
+ long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex();
+ long nextFrameSamplePosition = decoderJni.getDecodePosition();
+
+ boolean targetSampleInLastFrame =
+ lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
+ && nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
+
+ if (targetSampleInLastFrame) {
+ pendingSeekOperationParams = null;
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
+ pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
+ } else {
+ pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
+ }
+ }
+ }
+
+ private boolean skipInputUntilPosition(ExtractorInput input, long position)
+ throws IOException, InterruptedException {
+ long bytesToSkip = position - input.getPosition();
+ if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
+ input.skipFully((int) bytesToSkip);
+ return true;
+ }
+ return false;
+ }
+
+ private int seekToPosition(
+ ExtractorInput input, long position, PositionHolder seekPositionHolder) {
+ if (position == input.getPosition()) {
+ return Extractor.RESULT_CONTINUE;
+ } else {
+ seekPositionHolder.position = position;
+ return Extractor.RESULT_SEEK;
+ }
+ }
+
+ /**
+ * Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
+ *
+ *
This class holds parameters for a binary-search for the {@code targetSample} in the range
+ * [floorPosition, ceilingPosition).
+ */
+ private static final class SeekOperationParams {
+ private final long seekTimeUs;
+ private final long targetSample;
+ private final long approxBytesPerFrame;
+ private long floorSample;
+ private long ceilingSample;
+ private long floorPosition;
+ private long ceilingPosition;
+ private long nextSearchPosition;
+
+ private SeekOperationParams(
+ long seekTimeUs,
+ long targetSample,
+ long floorSample,
+ long ceilingSample,
+ long floorPosition,
+ long ceilingPosition,
+ long approxBytesPerFrame) {
+ this.seekTimeUs = seekTimeUs;
+ this.floorSample = floorSample;
+ this.ceilingSample = ceilingSample;
+ this.floorPosition = floorPosition;
+ this.ceilingPosition = ceilingPosition;
+ this.targetSample = targetSample;
+ this.approxBytesPerFrame = approxBytesPerFrame;
+ updateNextSearchPosition();
+ }
+
+ /** Updates the floor constraints (inclusive) of the seek operation. */
+ private void updateSeekFloor(long floorSample, long floorPosition) {
+ this.floorSample = floorSample;
+ this.floorPosition = floorPosition;
+ updateNextSearchPosition();
+ }
+
+ /** Updates the ceiling constraints (exclusive) of the seek operation. */
+ private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
+ this.ceilingSample = ceilingSample;
+ this.ceilingPosition = ceilingPosition;
+ updateNextSearchPosition();
+ }
+
+ private void updateNextSearchPosition() {
+ this.nextSearchPosition =
+ getNextSearchPosition(
+ targetSample,
+ floorSample,
+ ceilingSample,
+ floorPosition,
+ ceilingPosition,
+ approxBytesPerFrame);
+ }
+
+ /**
+ * Returns the next position in FLAC stream to search for target sample, given [floorPosition,
+ * ceilingPosition).
+ */
+ private static long getNextSearchPosition(
+ long targetSample,
+ long floorSample,
+ long ceilingSample,
+ long floorPosition,
+ long ceilingPosition,
+ long approxBytesPerFrame) {
+ if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
+ return floorPosition;
+ }
+ long samplesToSkip = targetSample - floorSample;
+ long estimatedBytesPerSample =
+ Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
+ // In the stream, the samples are accessed in a group of frame. Given a stream position, the
+ // seeker will be able to find the first frame following that position.
+ // Hence, if our target sample is in the middle of a frame, and our estimate position is
+ // correct, or very near the actual sample position, the seeker will keep accessing the next
+ // frame, rather than the frame that contains the target sample.
+ // Moreover, it's better to under-estimate rather than over-estimate, because the extractor
+ // input can skip forward easily, but cannot rewind easily (it may require a new connection
+ // to be made).
+ // Therefore, we should reduce the estimated position by some amount, so it will converge to
+ // the correct frame earlier.
+ long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
+ long confidenceInterval = bytesToSkip / 20;
+
+ long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
+ long estimatedPosition = estimatedFramePosition - confidenceInterval;
+
+ return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
+ }
+ }
+
+ /**
+ * A {@link SeekMap} implementation that returns the estimated byte location from {@link
+ * SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
+ * #getSeekPoints(long)} query.
+ */
+ private static final class FlacBinarySearchSeekMap implements SeekMap {
+ private final FlacStreamInfo streamInfo;
+ private final long firstFramePosition;
+ private final long inputLength;
+ private final long approxBytesPerFrame;
+ private final long durationUs;
+
+ private FlacBinarySearchSeekMap(
+ FlacStreamInfo streamInfo,
+ long firstFramePosition,
+ long inputLength,
+ long durationUs,
+ long approxBytesPerFrame) {
+ this.streamInfo = streamInfo;
+ this.firstFramePosition = firstFramePosition;
+ this.inputLength = inputLength;
+ this.approxBytesPerFrame = approxBytesPerFrame;
+ this.durationUs = durationUs;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ long nextSearchPosition =
+ SeekOperationParams.getNextSearchPosition(
+ streamInfo.getSampleIndex(timeUs),
+ /* floorSample= */ 0,
+ /* ceilingSample= */ streamInfo.totalSamples,
+ /* floorPosition= */ firstFramePosition,
+ /* ceilingPosition= */ inputLength,
+ approxBytesPerFrame);
+ return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+ }
+}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 15d294a35a..e8a04e06ae 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -92,18 +92,14 @@ import java.util.List;
}
decoderJni.setData(inputBuffer.data);
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
- int result;
try {
- result = decoderJni.decodeSample(outputData);
+ decoderJni.decodeSample(outputData);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ return new FlacDecoderException("Frame decoding failed", e);
} catch (IOException | InterruptedException e) {
// Never happens.
throw new IllegalStateException(e);
}
- if (result < 0) {
- return new FlacDecoderException("Frame decoding failed");
- }
- outputData.position(0);
- outputData.limit(result);
return null;
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
index ce787712da..69c0d082ee 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
@@ -26,6 +26,17 @@ import java.nio.ByteBuffer;
*/
/* package */ final class FlacDecoderJni {
+ /** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */
+ public static final class FlacFrameDecodeException extends Exception {
+
+ public final int errorCode;
+
+ public FlacFrameDecodeException(String message, int errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+ }
+
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private final long nativeDecoderContext;
@@ -116,14 +127,50 @@ import java.nio.ByteBuffer;
return byteCount;
}
+ /** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext);
}
- public int decodeSample(ByteBuffer output) throws IOException, InterruptedException {
- return output.isDirect()
- ? flacDecodeToBuffer(nativeDecoderContext, output)
- : flacDecodeToArray(nativeDecoderContext, output.array());
+ /**
+ * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
+ * error occurs, resets the stream and input to the given {@code retryPosition}.
+ *
+ * @param output The byte buffer to hold the decoded frame.
+ * @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
+ */
+ public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
+ throws InterruptedException, IOException, FlacFrameDecodeException {
+ try {
+ decodeSample(output);
+ } catch (IOException e) {
+ if (retryPosition >= 0) {
+ reset(retryPosition);
+ if (extractorInput != null) {
+ extractorInput.setRetryPosition(retryPosition, e);
+ }
+ }
+ throw e;
+ }
+ }
+
+ /** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
+ public void decodeSample(ByteBuffer output)
+ throws IOException, InterruptedException, FlacFrameDecodeException {
+ output.clear();
+ int frameSize =
+ output.isDirect()
+ ? flacDecodeToBuffer(nativeDecoderContext, output)
+ : flacDecodeToArray(nativeDecoderContext, output.array());
+ if (frameSize < 0) {
+ if (!isDecoderAtEndOfInput()) {
+ throw new FlacFrameDecodeException("Cannot decode FLAC frame", frameSize);
+ }
+ // The decoder has read to EOI. Return a 0-size frame to indicate the EOI.
+ output.limit(0);
+ } else {
+ output.limit(frameSize);
+ }
}
/**
@@ -133,8 +180,19 @@ import java.nio.ByteBuffer;
return flacGetDecodePosition(nativeDecoderContext);
}
- public long getLastSampleTimestamp() {
- return flacGetLastTimestamp(nativeDecoderContext);
+ /** Returns the timestamp for the first sample in the last decoded frame. */
+ public long getLastFrameTimestamp() {
+ return flacGetLastFrameTimestamp(nativeDecoderContext);
+ }
+
+ /** Returns the first sample index of the last extracted frame. */
+ public long getLastFrameFirstSampleIndex() {
+ return flacGetLastFrameFirstSampleIndex(nativeDecoderContext);
+ }
+
+ /** Returns the first sample index of the frame to be extracted next. */
+ public long getNextFrameFirstSampleIndex() {
+ return flacGetNextFrameFirstSampleIndex(nativeDecoderContext);
}
/**
@@ -153,6 +211,11 @@ import java.nio.ByteBuffer;
return flacGetStateString(nativeDecoderContext);
}
+ /** Returns whether the decoder has read to the end of the input. */
+ public boolean isDecoderAtEndOfInput() {
+ return flacIsDecoderAtEndOfStream(nativeDecoderContext);
+ }
+
public void flush() {
flacFlush(nativeDecoderContext);
}
@@ -181,18 +244,34 @@ import java.nio.ByteBuffer;
}
private native long flacInit();
+
private native FlacStreamInfo flacDecodeMetadata(long context)
throws IOException, InterruptedException;
+
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
throws IOException, InterruptedException;
+
private native int flacDecodeToArray(long context, byte[] outputArray)
throws IOException, InterruptedException;
+
private native long flacGetDecodePosition(long context);
- private native long flacGetLastTimestamp(long context);
+
+ private native long flacGetLastFrameTimestamp(long context);
+
+ private native long flacGetLastFrameFirstSampleIndex(long context);
+
+ private native long flacGetNextFrameFirstSampleIndex(long context);
+
private native long flacGetSeekPosition(long context, long timeUs);
+
private native String flacGetStateString(long context);
+
+ private native boolean flacIsDecoderAtEndOfStream(long context);
+
private native void flacFlush(long context);
+
private native void flacReset(long context, long newPosition);
+
private native void flacRelease(long context);
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index 34a6e6820d..a5efeb69f9 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor {
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
+ private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
+ private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
- private boolean metadataParsed;
+ private boolean readPastStreamInfo;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
@@ -136,83 +138,43 @@ public final class FlacExtractor implements Extractor {
}
decoderJni.setData(input);
+ readPastStreamInfo(input);
- if (!metadataParsed) {
- final FlacStreamInfo streamInfo;
- try {
- streamInfo = decoderJni.decodeMetadata();
- if (streamInfo == null) {
- throw new IOException("Metadata decoding failed");
- }
- } catch (IOException e) {
- decoderJni.reset(0);
- input.setRetryPosition(0, e);
- throw e; // never executes
- }
- metadataParsed = true;
-
- boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
- extractorOutput.seekMap(
- isSeekable
- ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
- : new SeekMap.Unseekable(streamInfo.durationUs(), 0));
- Format mediaFormat =
- Format.createAudioSampleFormat(
- /* id= */ null,
- MimeTypes.AUDIO_RAW,
- /* codecs= */ null,
- streamInfo.bitRate(),
- streamInfo.maxDecodedFrameSize(),
- streamInfo.channels,
- streamInfo.sampleRate,
- getPcmEncoding(streamInfo.bitsPerSample),
- /* encoderDelay= */ 0,
- /* encoderPadding= */ 0,
- /* initializationData= */ null,
- /* drmInitData= */ null,
- /* selectionFlags= */ 0,
- /* language= */ null,
- isId3MetadataDisabled ? null : id3Metadata);
- trackOutput.format(mediaFormat);
-
- outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
- outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
+ if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
+ return handlePendingSeek(input, seekPosition);
}
- outputBuffer.reset();
long lastDecodePosition = decoderJni.getDecodePosition();
- int size;
try {
- size = decoderJni.decodeSample(outputByteBuffer);
- } catch (IOException e) {
- if (lastDecodePosition >= 0) {
- decoderJni.reset(lastDecodePosition);
- input.setRetryPosition(lastDecodePosition, e);
- }
- throw e;
+ decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
- if (size <= 0) {
+ int outputSize = outputByteBuffer.limit();
+ if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
- trackOutput.sampleData(outputBuffer, size);
- trackOutput.sampleMetadata(decoderJni.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size,
- 0, null);
+ writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
- metadataParsed = false;
+ readPastStreamInfo = false;
}
if (decoderJni != null) {
decoderJni.reset(position);
}
+ if (flacBinarySearchSeeker != null) {
+ flacBinarySearchSeeker.setSeekTargetUs(timeUs);
+ }
}
@Override
public void release() {
+ flacBinarySearchSeeker = null;
if (decoderJni != null) {
decoderJni.release();
decoderJni = null;
@@ -244,6 +206,100 @@ public final class FlacExtractor implements Extractor {
return Arrays.equals(header, FLAC_SIGNATURE);
}
+ private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
+ if (readPastStreamInfo) {
+ return;
+ }
+
+ FlacStreamInfo streamInfo = decodeStreamInfo(input);
+ readPastStreamInfo = true;
+ if (this.streamInfo == null) {
+ updateFlacStreamInfo(input, streamInfo);
+ }
+ }
+
+ private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
+ this.streamInfo = streamInfo;
+ outputSeekMap(input, streamInfo);
+ outputFormat(streamInfo);
+ outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
+ outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
+ }
+
+ private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
+ throws InterruptedException, IOException {
+ try {
+ FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
+ if (streamInfo == null) {
+ throw new IOException("Metadata decoding failed");
+ }
+ return streamInfo;
+ } catch (IOException e) {
+ decoderJni.reset(0);
+ input.setRetryPosition(0, e);
+ throw e;
+ }
+ }
+
+ private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
+ boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
+ SeekMap seekMap =
+ hasSeekTable
+ ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
+ : getSeekMapForNonSeekTableFlac(input, streamInfo);
+ extractorOutput.seekMap(seekMap);
+ }
+
+ private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
+ long inputLength = input.getLength();
+ if (inputLength != C.LENGTH_UNSET) {
+ long firstFramePosition = decoderJni.getDecodePosition();
+ flacBinarySearchSeeker =
+ new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
+ return flacBinarySearchSeeker.getSeekMap();
+ } else { // can't seek at all, because there's no SeekTable and the input length is unknown.
+ return new SeekMap.Unseekable(streamInfo.durationUs());
+ }
+ }
+
+ private void outputFormat(FlacStreamInfo streamInfo) {
+ Format mediaFormat =
+ Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ streamInfo.bitRate(),
+ streamInfo.maxDecodedFrameSize(),
+ streamInfo.channels,
+ streamInfo.sampleRate,
+ getPcmEncoding(streamInfo.bitsPerSample),
+ /* encoderDelay= */ 0,
+ /* encoderPadding= */ 0,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ isId3MetadataDisabled ? null : id3Metadata);
+ trackOutput.format(mediaFormat);
+ }
+
+ private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
+ throws InterruptedException, IOException {
+ int seekResult =
+ flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
+ if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
+ writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
+ }
+ return seekResult;
+ }
+
+ private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
+ outputBuffer.setPosition(0);
+ trackOutput.sampleData(outputBuffer, size);
+ trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
+ }
+
+ /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
private static final class FlacSeekMap implements SeekMap {
private final long durationUs;
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index 59f37b0c2e..298719d48d 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {
return context->parser->getDecodePosition();
}
-DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) {
+DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {
Context *context = reinterpret_cast(jContext);
- return context->parser->getLastTimestamp();
+ return context->parser->getLastFrameTimestamp();
+}
+
+DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {
+ Context *context = reinterpret_cast(jContext);
+ return context->parser->getLastFrameFirstSampleIndex();
+}
+
+DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
+ Context *context = reinterpret_cast(jContext);
+ return context->parser->getNextFrameFirstSampleIndex();
}
DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
return env->NewStringUTF(str);
}
+DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {
+ Context *context = reinterpret_cast(jContext);
+ return context->parser->isDecoderAtEndOfStream();
+}
+
DECODER_FUNC(void, flacFlush, jlong jContext) {
Context *context = reinterpret_cast(jContext);
context->parser->flush();
diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h
index 8a769b66d4..cea7fbe33b 100644
--- a/extensions/flac/src/main/jni/include/flac_parser.h
+++ b/extensions/flac/src/main/jni/include/flac_parser.h
@@ -44,10 +44,18 @@ class FLACParser {
return mStreamInfo;
}
- int64_t getLastTimestamp() const {
+ int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
+ int64_t getLastFrameFirstSampleIndex() const {
+ return mWriteHeader.number.sample_number;
+ }
+
+ int64_t getNextFrameFirstSampleIndex() const {
+ return mWriteHeader.number.sample_number + mWriteHeader.blocksize;
+ }
+
bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size);
@@ -83,6 +91,11 @@ class FLACParser {
return FLAC__stream_decoder_get_resolved_state_string(mDecoder);
}
+ bool isDecoderAtEndOfStream() const {
+ return FLAC__stream_decoder_get_state(mDecoder) ==
+ FLAC__STREAM_DECODER_END_OF_STREAM;
+ }
+
private:
DataSource *mDataSource;
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index d3dbaaec96..2d9ddfb288 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -649,18 +649,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void loadAd(String adUriString) {
- if (adGroupIndex == C.INDEX_UNSET) {
- Log.w(
- TAG,
- "Unexpected loadAd without LOADED event; assuming ad group index is actually "
- + expectedAdGroupIndex);
- adGroupIndex = expectedAdGroupIndex;
- adsManager.start();
- }
- if (DEBUG) {
- Log.d(TAG, "loadAd in ad group " + adGroupIndex);
- }
try {
+ if (adGroupIndex == C.INDEX_UNSET) {
+ Log.w(
+ TAG,
+ "Unexpected loadAd without LOADED event; assuming ad group index is actually "
+ + expectedAdGroupIndex);
+ adGroupIndex = expectedAdGroupIndex;
+ adsManager.start();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "loadAd in ad group " + adGroupIndex);
+ }
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index 172159b7af..f2898005c1 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource {
// Check for a valid response code.
if (!response.isSuccessful()) {
- Map> headers = request.headers().toMultimap();
+ Map> headers = response.headers().toMultimap();
closeConnectionQuietly();
InvalidResponseCodeException exception = new InvalidResponseCodeException(
responseCode, headers, dataSpec);
diff --git a/library/core/build.gradle b/library/core/build.gradle
index 52249220e0..bb331b615c 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -22,6 +22,13 @@ android {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ // The following argument makes the Android Test Orchestrator run its
+ // "pm clear" command after each test invocation. This command ensures
+ // that the app's state is completely cleared between tests.
+ testInstrumentationRunnerArguments clearPackageData: 'true'
}
// Workaround to prevent circular dependency on project :testutils.
@@ -42,19 +49,17 @@ android {
// testCoverageEnabled = true
// }
}
-
- lintOptions {
- lintConfig file("../../checker-framework-lint.xml")
- }
}
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
+ androidTestImplementation 'com.android.support.test:runner:' + testRunnerVersion
+ androidTestUtil 'com.android.support.test:orchestrator:' + testRunnerVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
index 3465393853..1133928e91 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.upstream;
import static com.google.common.truth.Truth.assertThat;
+import static junit.framework.Assert.fail;
-import android.app.Instrumentation;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -28,48 +28,58 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.test.InstrumentationTestCase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
-/**
- * Unit tests for {@link ContentDataSource}.
- */
-public final class ContentDataSourceTest extends InstrumentationTestCase {
+/** Unit tests for {@link ContentDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public final class ContentDataSourceTest {
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
+ @Test
public void testRead() throws Exception {
- assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false);
+ assertData(0, C.LENGTH_UNSET, false);
}
+ @Test
public void testReadPipeMode() throws Exception {
- assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true);
+ assertData(0, C.LENGTH_UNSET, true);
}
+ @Test
public void testReadFixedLength() throws Exception {
- assertData(getInstrumentation(), 0, 100, false);
+ assertData(0, 100, false);
}
+ @Test
public void testReadFromOffsetToEndOfInput() throws Exception {
- assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false);
+ assertData(1, C.LENGTH_UNSET, false);
}
+ @Test
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
- assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true);
+ assertData(1, C.LENGTH_UNSET, true);
}
+ @Test
public void testReadFromOffsetFixedLength() throws Exception {
- assertData(getInstrumentation(), 1, 100, false);
+ assertData(1, 100, false);
}
+ @Test
public void testReadInvalidUri() throws Exception {
- ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
+ ContentDataSource dataSource =
+ new ContentDataSource(InstrumentationRegistry.getTargetContext());
Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
DataSpec dataSpec = new DataSpec(contentUri);
try {
@@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
}
}
- private static void assertData(Instrumentation instrumentation, int offset, int length,
- boolean pipeMode) throws IOException {
+ private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
- ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
+ ContentDataSource dataSource =
+ new ContentDataSource(InstrumentationRegistry.getTargetContext());
try {
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
- byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH);
+ byte[] completeData =
+ TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH);
byte[] expectedData = Arrays.copyOfRange(completeData, offset,
length == C.LENGTH_UNSET ? completeData.length : offset + length);
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
index 58531346ab..be4a2a96dc 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
@@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.net.Uri;
-import android.test.InstrumentationTestCase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
@@ -29,9 +30,14 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
/** Tests {@link CachedContentIndex}. */
-public class CachedContentIndexTest extends InstrumentationTestCase {
+@RunWith(AndroidJUnit4.class)
+public class CachedContentIndexTest {
private final byte[] testIndexV1File = {
0, 0, 0, 1, // version
@@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
private CachedContentIndex index;
private File cacheDir;
- @Override
+ @Before
public void setUp() throws Exception {
- super.setUp();
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ cacheDir =
+ Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
- @Override
- protected void tearDown() throws Exception {
+ @After
+ public void tearDown() {
Util.recursiveDelete(cacheDir);
- super.tearDown();
}
+ @Test
public void testAddGetRemove() throws Exception {
final String key1 = "key1";
final String key2 = "key2";
@@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(cacheSpanFile.exists()).isTrue();
}
+ @Test
public void testStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
}
+ @Test
public void testLoadV1() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV1File);
@@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
}
+ @Test
public void testLoadV2() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV2File);
@@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
}
- public void testAssignIdForKeyAndGetKeyForId() throws Exception {
+ @Test
+ public void testAssignIdForKeyAndGetKeyForId() {
final String key1 = "key1";
final String key2 = "key2";
int id1 = index.assignIdForKey(key1);
@@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
}
- public void testGetNewId() throws Exception {
+ @Test
+ public void testGetNewId() {
SparseArray idToKey = new SparseArray<>();
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
idToKey.put(10, "");
@@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
}
+ @Test
public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
@@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
}
- public void testRemoveEmptyNotLockedCachedContent() throws Exception {
+ @Test
+ public void testRemoveEmptyNotLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1");
index.maybeRemove(cachedContent.key);
@@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNull();
}
+ @Test
public void testCantRemoveNotEmptyCachedContent() throws Exception {
CachedContent cachedContent = index.getOrAdd("key1");
File cacheSpanFile =
@@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNotNull();
}
- public void testCantRemoveLockedCachedContent() throws Exception {
+ @Test
+ public void testCantRemoveLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true);
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
index 637a19cdd2..afbbf6605f 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.test.InstrumentationTestCase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileOutputStream;
@@ -26,11 +27,14 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
-/**
- * Unit tests for {@link SimpleCacheSpan}.
- */
-public class SimpleCacheSpanTest extends InstrumentationTestCase {
+/** Unit tests for {@link SimpleCacheSpan}. */
+@RunWith(AndroidJUnit4.class)
+public class SimpleCacheSpanTest {
private CachedContentIndex index;
private File cacheDir;
@@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
}
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
+ @Before
+ public void setUp() throws Exception {
+ cacheDir =
+ Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
- @Override
- protected void tearDown() throws Exception {
+ @After
+ public void tearDown() {
Util.recursiveDelete(cacheDir);
- super.tearDown();
}
+ @Test
public void testCacheFile() throws Exception {
assertCacheSpan("key1", 0, 0);
assertCacheSpan("key2", 1, 2);
@@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
+ "A paragraph-separator character \u2029", 1, 2);
}
+ @Test
public void testUpgradeFileName() throws Exception {
String key = "asd\u00aa";
int id = index.assignIdForKey(key);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
index b5b364a327..f8b7f5f5c2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -46,11 +46,10 @@ public class DefaultLoadControl implements LoadControl {
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500;
/**
- * The default duration of media that must be buffered for playback to resume after a rebuffer,
- * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
- * action.
+ * The default duration of media that must be buffered for playback to resume after a rebuffer, in
+ * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.
*/
- public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
+ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
/**
* The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 6d8dd5b7a8..39a6243933 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -185,10 +185,6 @@ public interface ExoPlayer extends Player {
*/
Looper getPlaybackLooper();
- @Override
- @Nullable
- ExoPlaybackException getPlaybackError();
-
/**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index 5ca5994b6e..4125a203a6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
internalPlayer.setPlayWhenReady(playWhenReady);
+ PlaybackInfo playbackInfo = this.playbackInfo;
for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
}
@@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
break;
case ExoPlayerImplInternal.MSG_ERROR:
- playbackError = (ExoPlaybackException) msg.obj;
+ ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
+ this.playbackError = playbackError;
for (Player.EventListener listener : listeners) {
listener.onPlayerError(playbackError);
}
@@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
boolean trackSelectorResultChanged =
- this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
+ playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
playbackInfo = newPlaybackInfo;
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
for (Player.EventListener listener : listeners) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index ceee25af82..fc946804f4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -854,6 +854,9 @@ import java.util.Collections;
}
private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
+ if (message.isCanceled()) {
+ return;
+ }
try {
message.getTarget().handleMessage(message.getType(), message.getPayload());
} finally {
@@ -945,7 +948,7 @@ import java.util.Collections;
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
sendMessageToTarget(nextInfo.message);
- if (nextInfo.message.getDeleteAfterDelivery()) {
+ if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
pendingMessages.remove(nextPendingMessageIndex);
} else {
nextPendingMessageIndex++;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index 98d5fe91b7..aabb01481b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.8.0";
+ public static final String VERSION = "2.8.1";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2008000;
+ public static final int VERSION_INT = 2008001;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
index a7de96a2de..6f2db4ff5e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
/**
@@ -87,7 +88,7 @@ public final class PlaybackParameters {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
index 408cbecaf1..2c7aee834e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
@@ -63,6 +63,7 @@ public final class PlayerMessage {
private boolean isSent;
private boolean isDelivered;
private boolean isProcessed;
+ private boolean isCanceled;
/**
* Creates a new message.
@@ -242,6 +243,24 @@ public final class PlayerMessage {
return this;
}
+ /**
+ * Cancels the message delivery.
+ *
+ * @return This message.
+ * @throws IllegalStateException If this method is called before {@link #send()}.
+ */
+ public synchronized PlayerMessage cancel() {
+ Assertions.checkState(isSent);
+ isCanceled = true;
+ markAsProcessed(/* isDelivered= */ false);
+ return this;
+ }
+
+ /** Returns whether the message delivery has been canceled. */
+ public synchronized boolean isCanceled() {
+ return isCanceled;
+ }
+
/**
* Blocks until after the message has been delivered or the player is no longer able to deliver
* the message.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java
index 93bbd1e4b6..684072efc6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
+
/**
* The configuration of a {@link Renderer}.
*/
@@ -41,7 +43,7 @@ public final class RendererConfiguration {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
index 2df9840cf8..ca0433f96d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
/**
@@ -71,7 +72,7 @@ public final class SeekParameters {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 482e2c970a..0a0df03053 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
private AudioAttributes audioAttributes;
private float audioVolume;
private MediaSource mediaSource;
+ private List currentCues;
/**
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
@@ -177,6 +178,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioAttributes = AudioAttributes.DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+ currentCues = Collections.emptyList();
// Build the player and associated objects.
player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
@@ -502,6 +504,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void addTextOutput(TextOutput listener) {
+ if (!currentCues.isEmpty()) {
+ listener.onCues(currentCues);
+ }
textOutputs.add(listener);
}
@@ -775,6 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
mediaSource = null;
analyticsCollector.resetForNewMediaSource();
}
+ currentCues = Collections.emptyList();
}
@Override
@@ -790,6 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector);
}
+ currentCues = Collections.emptyList();
}
@Override
@@ -1095,6 +1102,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override
public void onCues(List cues) {
+ currentCues = cues;
for (TextOutput textOutput : textOutputs) {
textOutput.onCues(cues);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
index 337200da8f..5e963a2540 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio;
import android.annotation.TargetApi;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
/**
@@ -119,7 +120,7 @@ public final class AudioAttributes {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
index 499ea488c7..4b03a5047b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java
@@ -22,6 +22,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioFormat;
import android.media.AudioManager;
+import android.support.annotation.Nullable;
import java.util.Arrays;
/**
@@ -96,7 +97,7 @@ public final class AudioCapabilities {
}
@Override
- public boolean equals(Object other) {
+ public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
index 4a59667dc8..c2de662010 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator, Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
@@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator, Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SchemeData)) {
return false;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java
index aa718c23e5..b7aaa2a31b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
@@ -92,7 +93,7 @@ public interface SeekMap {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java
index 93cfbd9200..8b920bc024 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor;
+import android.support.annotation.Nullable;
+
/** Defines a seek point in a media stream. */
public final class SeekPoint {
@@ -42,7 +44,7 @@ public final class SeekPoint {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
index a12a0315a4..6a8cef6b64 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -69,7 +70,7 @@ public interface TrackOutput {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index a6e2524f0b..a2b787d6b0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -189,11 +189,13 @@ import java.util.List;
}
}
- // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio.
- boolean isRechunkable = sampleSizeBox.isFixedSampleSize()
- && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
- && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0
- && remainingSynchronizationSamples == 0;
+ // Fixed sample size raw audio may need to be rechunked.
+ boolean isFixedSampleSizeRawAudio =
+ sampleSizeBox.isFixedSampleSize()
+ && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
+ && remainingTimestampDeltaChanges == 0
+ && remainingTimestampOffsetChanges == 0
+ && remainingSynchronizationSamples == 0;
long[] offsets;
int[] sizes;
@@ -203,7 +205,7 @@ import java.util.List;
long timestampTimeUnits = 0;
long duration;
- if (!isRechunkable) {
+ if (!isFixedSampleSizeRawAudio) {
offsets = new long[sampleCount];
sizes = new int[sampleCount];
timestamps = new long[sampleCount];
@@ -296,7 +298,8 @@ import java.util.List;
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
}
- int fixedSampleSize = sampleSizeBox.readNextSampleSize();
+ int fixedSampleSize =
+ Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
offsets = rechunkedResults.offsets;
@@ -1224,7 +1227,7 @@ import java.util.List;
stsc.setPosition(Atom.FULL_HEADER_SIZE);
remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
- index = C.INDEX_UNSET;
+ index = -1;
}
public boolean moveNext() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index 49f7361bc5..347afe29fd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -482,13 +482,13 @@ public final class MediaCodecUtil {
return null;
}
- Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger);
- if (profile == null) {
+ int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);
+ if (profile == -1) {
Log.w(TAG, "Unknown AVC profile: " + profileInteger);
return null;
}
- Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger);
- if (level == null) {
+ int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);
+ if (level == -1) {
Log.w(TAG, "Unknown AVC level: " + levelInteger);
return null;
}
@@ -639,7 +639,7 @@ public final class MediaCodecUtil {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
index a8c9d0b5a8..a2ad7fe2ce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
@@ -76,7 +77,7 @@ public final class Metadata implements Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
index 0612c18e18..5f521aada6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
index eafb0286ce..ae78f712c7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
index f662c1d06f..129803299c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import java.util.Arrays;
/**
@@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
index c82f982aa7..aca530cdee 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
index 939c00b9db..56b08bbee3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
index b43a46349c..e84b776790 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
/**
@@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
index 0ed429055b..8b665fce00 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
index db6db2ea4f..1b5ba67c11 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
index 3374db5d8d..dbab4ca7a8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
/**
@@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
index 775ab5dd3e..f657eefc30 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
/**
@@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
index cf061f3745..98360b909c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
@@ -140,7 +140,7 @@ public abstract class DownloadAction {
DownloaderConstructorHelper downloaderConstructorHelper);
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
index 8be822b6ca..0e2c5874b1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
@@ -250,7 +251,6 @@ public final class DownloadManager {
Assertions.checkState(!released);
Task task = addTaskForAction(action);
if (initialized) {
- notifyListenersTaskStateChange(task);
saveActions();
maybeStartTasks();
if (task.currentState == STATE_QUEUED) {
@@ -413,7 +413,6 @@ public final class DownloadManager {
if (released) {
return;
}
- logd("Task state is changed", task);
boolean stopped = !task.isActive();
if (stopped) {
activeDownloadTasks.remove(task);
@@ -430,6 +429,7 @@ public final class DownloadManager {
}
private void notifyListenersTaskStateChange(Task task) {
+ logd("Task state is changed", task);
TaskState taskState = task.getDownloadState();
for (Listener listener : listeners) {
listener.onTaskStateChanged(this, taskState);
@@ -468,18 +468,16 @@ public final class DownloadManager {
listener.onInitialized(DownloadManager.this);
}
if (!pendingTasks.isEmpty()) {
- for (int i = 0; i < pendingTasks.size(); i++) {
- tasks.add(pendingTasks.get(i));
- }
+ tasks.addAll(pendingTasks);
saveActions();
}
maybeStartTasks();
- for (int i = 0; i < pendingTasks.size(); i++) {
- Task pendingTask = pendingTasks.get(i);
- if (pendingTask.currentState == STATE_QUEUED) {
+ for (int i = 0; i < tasks.size(); i++) {
+ Task task = tasks.get(i);
+ if (task.currentState == STATE_QUEUED) {
// Task did not change out of its initial state, and so its initial state
// won't have been reported to listeners. Do so now.
- notifyListenersTaskStateChange(pendingTask);
+ notifyListenersTaskStateChange(task);
}
}
}
@@ -699,9 +697,19 @@ public final class DownloadManager {
+ ' '
+ (action.isRemoveAction ? "remove" : "download")
+ ' '
+ + toString(action.data)
+ + ' '
+ getStateString();
}
+ private static String toString(byte[] data) {
+ if (data.length > 100) {
+ return "";
+ } else {
+ return '\'' + Util.fromUtf8Bytes(data) + '\'';
+ }
+ }
+
private String getStateString() {
switch (currentState) {
case STATE_QUEUED_CANCELING:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java
index 02ef7a7aa7..d8db6f96c2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java
@@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction {
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java
index f6a32a1253..ae57131641 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java
@@ -112,7 +112,7 @@ public abstract class SegmentDownloadAction> extends Dow
protected abstract void writeKey(DataOutputStream output, K key) throws IOException;
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
index f8c2f8b3e1..1a243a8bf0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -145,7 +145,7 @@ public interface MediaSource {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
index 2e5b259a88..a9fb261768 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
@@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
index 72afa3463e..a155032a9f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.util.Arrays;
@@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java
index 5ae1f35b7e..8bc0b8e136 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java
@@ -78,6 +78,25 @@ public class Cue {
*/
public static final int LINE_TYPE_NUMBER = 1;
+ /** The type of default text size for this cue, which may be unset. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_UNSET,
+ TEXT_SIZE_TYPE_FRACTIONAL,
+ TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
+ TEXT_SIZE_TYPE_ABSOLUTE
+ })
+ public @interface TextSizeType {}
+
+ /** Text size is measured as a fraction of the viewport size minus the view padding. */
+ public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0;
+
+ /** Text size is measured as a fraction of the viewport size, ignoring the view padding */
+ public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1;
+
+ /** Text size is measured in number of pixels. */
+ public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2;
+
/**
* The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated
* with styling spans.
@@ -106,40 +125,39 @@ public class Cue {
/**
* The type of the {@link #line} value.
- *
- * {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
+ *
+ *
{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
* viewport.
- *
- * {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
- * line is taken to be the size of the first line of the cue. When {@link #line} is greater than
- * or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from
- * the start edge. When {@link #line} is negative lines count from the end of the viewport, with
- * -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height
- * of the first line of the cue, and the start and end of the viewport are the top and bottom
- * respectively.
- *
- * Note that it's particularly important to consider the effect of {@link #lineAnchor} when using
- * {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a
- * (potentially multi-line) cue at the very top of the viewport.
- * {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue
- * at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)}
- * and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of
- * the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only
- * the last line is visible at the top of the viewport.
- * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first
- * line is visible at the bottom of the viewport.
+ *
+ *
{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of
+ * each line is taken to be the size of the first line of the cue. When {@link #line} is greater
+ * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset
+ * from the start edge. When {@link #line} is negative lines count from the end of the viewport,
+ * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the
+ * height of the first line of the cue, and the start and end of the viewport are the top and
+ * bottom respectively.
+ *
+ *
Note that it's particularly important to consider the effect of {@link #lineAnchor} when
+ * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)}
+ * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 &&
+ * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of
+ * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 &&
+ * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line
+ * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible
+ * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a
+ * cue so that only its first line is visible at the bottom of the viewport.
*/
- @LineType public final int lineType;
+ public final @LineType int lineType;
/**
- * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
- * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
- *
- * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
- * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
- * respectively.
+ * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link
+ * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ *
+ *
For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
+ * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of
+ * the cue box respectively.
*/
- @AnchorType public final int lineAnchor;
+ public final @AnchorType int lineAnchor;
/**
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
@@ -152,14 +170,14 @@ public class Cue {
public final float position;
/**
- * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
- * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
- *
- * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
- * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
- * respectively.
+ * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link
+ * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ *
+ *
For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
+ * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of
+ * the cue box respectively.
*/
- @AnchorType public final int positionAnchor;
+ public final @AnchorType int positionAnchor;
/**
* The size of the cue box in the writing direction specified as a fraction of the viewport size
@@ -184,6 +202,18 @@ public class Cue {
*/
public final int windowColor;
+ /**
+ * The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no
+ * default text size.
+ */
+ public final @TextSizeType int textSizeType;
+
+ /**
+ * The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default
+ * text size.
+ */
+ public final float textSize;
+
/**
* Creates an image cue.
*
@@ -194,17 +224,36 @@ public class Cue {
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* @param verticalPosition The position of the vertical anchor within the viewport, expressed as a
* fraction of the viewport height.
- * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START},
- * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
+ * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link
+ * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* @param width The width of the cue as a fraction of the viewport width.
- * @param height The height of the cue as a fraction of the viewport height, or
- * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the
- * specified {@code width}.
+ * @param height The height of the cue as a fraction of the viewport height, or {@link
+ * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified
+ * {@code width}.
*/
- public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor,
- float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) {
- this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor,
- horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK);
+ public Cue(
+ Bitmap bitmap,
+ float horizontalPosition,
+ @AnchorType int horizontalPositionAnchor,
+ float verticalPosition,
+ @AnchorType int verticalPositionAnchor,
+ float width,
+ float height) {
+ this(
+ /* text= */ null,
+ /* textAlignment= */ null,
+ bitmap,
+ verticalPosition,
+ /* lineType= */ LINE_TYPE_FRACTION,
+ verticalPositionAnchor,
+ horizontalPosition,
+ horizontalPositionAnchor,
+ /* textSizeType= */ TYPE_UNSET,
+ /* textSize= */ DIMEN_UNSET,
+ width,
+ height,
+ /* windowColorSet= */ false,
+ /* windowColor= */ Color.BLACK);
}
/**
@@ -214,7 +263,15 @@ public class Cue {
* @param text See {@link #text}.
*/
public Cue(CharSequence text) {
- this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
+ this(
+ text,
+ /* textAlignment= */ null,
+ /* line= */ DIMEN_UNSET,
+ /* lineType= */ TYPE_UNSET,
+ /* lineAnchor= */ TYPE_UNSET,
+ /* position= */ DIMEN_UNSET,
+ /* positionAnchor= */ TYPE_UNSET,
+ /* size= */ DIMEN_UNSET);
}
/**
@@ -229,10 +286,68 @@ public class Cue {
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
*/
- public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
- @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) {
- this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false,
- Color.BLACK);
+ public Cue(
+ CharSequence text,
+ Alignment textAlignment,
+ float line,
+ @LineType int lineType,
+ @AnchorType int lineAnchor,
+ float position,
+ @AnchorType int positionAnchor,
+ float size) {
+ this(
+ text,
+ textAlignment,
+ line,
+ lineType,
+ lineAnchor,
+ position,
+ positionAnchor,
+ size,
+ /* windowColorSet= */ false,
+ /* windowColor= */ Color.BLACK);
+ }
+
+ /**
+ * Creates a text cue.
+ *
+ * @param text See {@link #text}.
+ * @param textAlignment See {@link #textAlignment}.
+ * @param line See {@link #line}.
+ * @param lineType See {@link #lineType}.
+ * @param lineAnchor See {@link #lineAnchor}.
+ * @param position See {@link #position}.
+ * @param positionAnchor See {@link #positionAnchor}.
+ * @param size See {@link #size}.
+ * @param textSizeType See {@link #textSizeType}.
+ * @param textSize See {@link #textSize}.
+ */
+ public Cue(
+ CharSequence text,
+ Alignment textAlignment,
+ float line,
+ @LineType int lineType,
+ @AnchorType int lineAnchor,
+ float position,
+ @AnchorType int positionAnchor,
+ float size,
+ @TextSizeType int textSizeType,
+ float textSize) {
+ this(
+ text,
+ textAlignment,
+ /* bitmap= */ null,
+ line,
+ lineType,
+ lineAnchor,
+ position,
+ positionAnchor,
+ textSizeType,
+ textSize,
+ size,
+ /* bitmapHeight= */ DIMEN_UNSET,
+ /* windowColorSet= */ false,
+ /* windowColor= */ Color.BLACK);
}
/**
@@ -249,16 +364,48 @@ public class Cue {
* @param windowColorSet See {@link #windowColorSet}.
* @param windowColor See {@link #windowColor}.
*/
- public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
- @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
- boolean windowColorSet, int windowColor) {
- this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size,
- DIMEN_UNSET, windowColorSet, windowColor);
+ public Cue(
+ CharSequence text,
+ Alignment textAlignment,
+ float line,
+ @LineType int lineType,
+ @AnchorType int lineAnchor,
+ float position,
+ @AnchorType int positionAnchor,
+ float size,
+ boolean windowColorSet,
+ int windowColor) {
+ this(
+ text,
+ textAlignment,
+ /* bitmap= */ null,
+ line,
+ lineType,
+ lineAnchor,
+ position,
+ positionAnchor,
+ /* textSizeType= */ TYPE_UNSET,
+ /* textSize= */ DIMEN_UNSET,
+ size,
+ /* bitmapHeight= */ DIMEN_UNSET,
+ windowColorSet,
+ windowColor);
}
- private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line,
- @LineType int lineType, @AnchorType int lineAnchor, float position,
- @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet,
+ private Cue(
+ CharSequence text,
+ Alignment textAlignment,
+ Bitmap bitmap,
+ float line,
+ @LineType int lineType,
+ @AnchorType int lineAnchor,
+ float position,
+ @AnchorType int positionAnchor,
+ @TextSizeType int textSizeType,
+ float textSize,
+ float size,
+ float bitmapHeight,
+ boolean windowColorSet,
int windowColor) {
this.text = text;
this.textAlignment = textAlignment;
@@ -272,6 +419,8 @@ public class Cue {
this.bitmapHeight = bitmapHeight;
this.windowColorSet = windowColorSet;
this.windowColor = windowColor;
+ this.textSizeType = textSizeType;
+ this.textSize = textSize;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
index a215bf3cc9..ad8f849c60 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
/**
* A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
* supported by this decoder are:
+ *
*
* - content
*
- core
@@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
*
- time-clock
*
- time-offset-with-frames
*
- time-offset-with-ticks
+ *
- cell-resolution
*
+ *
* @see TTML specification
*/
public final class TtmlDecoder extends SimpleSubtitleDecoder {
@@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
+ private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
private static final int DEFAULT_FRAME_RATE = 30;
private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
+ private static final CellResolution DEFAULT_CELL_RESOLUTION =
+ new CellResolution(/* columns= */ 32, /* rows= */ 15);
private final XmlPullParserFactory xmlParserFactory;
@@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int unsupportedNodeDepth = 0;
int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
+ CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peekLast();
if (unsupportedNodeDepth == 0) {
@@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (eventType == XmlPullParser.START_TAG) {
if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser);
+ cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
}
if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) {
- parseHeader(xmlParser, globalStyles, regionMap);
+ parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
} else {
try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
@@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
}
- private Map parseHeader(XmlPullParser xmlParser,
- Map globalStyles, Map globalRegions)
+ private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
+ throws SubtitleDecoderException {
+ String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
+ if (cellResolution == null) {
+ return defaultValue;
+ }
+
+ Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution);
+ if (!cellResolutionMatcher.matches()) {
+ Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
+ return defaultValue;
+ }
+ try {
+ int columns = Integer.parseInt(cellResolutionMatcher.group(1));
+ int rows = Integer.parseInt(cellResolutionMatcher.group(2));
+ if (columns == 0 || rows == 0) {
+ throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows);
+ }
+ return new CellResolution(columns, rows);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
+ return defaultValue;
+ }
+ }
+
+ private Map parseHeader(
+ XmlPullParser xmlParser,
+ Map globalStyles,
+ Map globalRegions,
+ CellResolution cellResolution)
throws IOException, XmlPullParserException {
do {
xmlParser.next();
@@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style);
}
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
- TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser);
+ TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion);
}
@@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
/**
* Parses a region declaration.
- *
- * If the region defines an origin and extent, it is required that they're defined as percentages
- * of the viewport. Region declarations that define origin and extent in other formats are
- * unsupported, and null is returned.
+ *
+ *
If the region defines an origin and extent, it is required that they're defined as
+ * percentages of the viewport. Region declarations that define origin and extent in other formats
+ * are unsupported, and null is returned.
*/
- private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) {
+ private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) {
return null;
@@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
}
- return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width);
+ float regionTextHeight = 1.0f / cellResolution.rows;
+ return new TtmlRegion(
+ regionId,
+ position,
+ line,
+ /* lineType= */ Cue.LINE_TYPE_FRACTION,
+ lineAnchor,
+ width,
+ /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
+ /* textSize= */ regionTextHeight);
}
private String[] parseStyleIds(String parentStyleIds) {
@@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.tickRate = tickRate;
}
}
+
+ /** Represents the cell resolution for a TTML file. */
+ private static final class CellResolution {
+ final int columns;
+ final int rows;
+
+ CellResolution(int columns, int rows) {
+ this.columns = columns;
+ this.rows = rows;
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
index 43fa7a1bd9..c8b9a59de4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -175,35 +175,51 @@ import java.util.TreeSet;
Map regionMap) {
TreeMap regionOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionOutputs);
- traverseForStyle(globalStyles, regionOutputs);
+ traverseForStyle(timeUs, globalStyles, regionOutputs);
List cues = new ArrayList<>();
for (Entry entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey());
- cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
- region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width));
+ cues.add(
+ new Cue(
+ cleanUpText(entry.getValue()),
+ /* textAlignment= */ null,
+ region.line,
+ region.lineType,
+ region.lineAnchor,
+ region.position,
+ /* positionAnchor= */ Cue.TYPE_UNSET,
+ region.width,
+ region.textSizeType,
+ region.textSize));
}
return cues;
}
- private void traverseForText(long timeUs, boolean descendsPNode,
- String inheritedRegion, Map regionOutputs) {
+ private void traverseForText(
+ long timeUs,
+ boolean descendsPNode,
+ String inheritedRegion,
+ Map regionOutputs) {
nodeStartsByRegion.clear();
nodeEndsByRegion.clear();
- String resolvedRegionId = regionId;
- if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) {
- resolvedRegionId = inheritedRegion;
+ if (TAG_METADATA.equals(tag)) {
+ // Ignore metadata tag.
+ return;
}
+
+ String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
+
if (isTextNode && descendsPNode) {
getRegionOutput(resolvedRegionId, regionOutputs).append(text);
} else if (TAG_BR.equals(tag) && descendsPNode) {
getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
- } else if (TAG_METADATA.equals(tag)) {
- // Do nothing.
} else if (isActive(timeUs)) {
- boolean isPNode = TAG_P.equals(tag);
+ // This is a container node, which can contain zero or more children.
for (Entry entry : regionOutputs.entrySet()) {
nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
}
+
+ boolean isPNode = TAG_P.equals(tag);
for (int i = 0; i < getChildCount(); i++) {
getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
regionOutputs);
@@ -211,39 +227,50 @@ import java.util.TreeSet;
if (isPNode) {
TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
}
+
for (Entry entry : regionOutputs.entrySet()) {
nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
}
}
}
- private static SpannableStringBuilder getRegionOutput(String resolvedRegionId,
- Map regionOutputs) {
+ private static SpannableStringBuilder getRegionOutput(
+ String resolvedRegionId, Map regionOutputs) {
if (!regionOutputs.containsKey(resolvedRegionId)) {
regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
}
return regionOutputs.get(resolvedRegionId);
}
- private void traverseForStyle(Map globalStyles,
+ private void traverseForStyle(
+ long timeUs,
+ Map globalStyles,
Map regionOutputs) {
+ if (!isActive(timeUs)) {
+ return;
+ }
for (Entry entry : nodeEndsByRegion.entrySet()) {
String regionId = entry.getKey();
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
- applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue());
- for (int i = 0; i < getChildCount(); ++i) {
- getChild(i).traverseForStyle(globalStyles, regionOutputs);
+ int end = entry.getValue();
+ if (start != end) {
+ SpannableStringBuilder regionOutput = regionOutputs.get(regionId);
+ applyStyleToOutput(globalStyles, regionOutput, start, end);
}
}
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
+ }
}
- private void applyStyleToOutput(Map globalStyles,
- SpannableStringBuilder regionOutput, int start, int end) {
- if (start != end) {
- TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
- if (resolvedStyle != null) {
- TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
- }
+ private void applyStyleToOutput(
+ Map globalStyles,
+ SpannableStringBuilder regionOutput,
+ int start,
+ int end) {
+ TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
+ if (resolvedStyle != null) {
+ TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
index 98823d7a84..2b1e9cf99a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
@@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue;
public final String id;
public final float position;
public final float line;
- @Cue.LineType public final int lineType;
- @Cue.AnchorType public final int lineAnchor;
+ public final @Cue.LineType int lineType;
+ public final @Cue.AnchorType int lineAnchor;
public final float width;
+ public final @Cue.TextSizeType int textSizeType;
+ public final float textSize;
public TtmlRegion(String id) {
- this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
+ this(
+ id,
+ /* position= */ Cue.DIMEN_UNSET,
+ /* line= */ Cue.DIMEN_UNSET,
+ /* lineType= */ Cue.TYPE_UNSET,
+ /* lineAnchor= */ Cue.TYPE_UNSET,
+ /* width= */ Cue.DIMEN_UNSET,
+ /* textSizeType= */ Cue.TYPE_UNSET,
+ /* textSize= */ Cue.DIMEN_UNSET);
}
- public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType,
- @Cue.AnchorType int lineAnchor, float width) {
+ public TtmlRegion(
+ String id,
+ float position,
+ float line,
+ @Cue.LineType int lineType,
+ @Cue.AnchorType int lineAnchor,
+ float width,
+ int textSizeType,
+ float textSize) {
this.id = id;
this.position = position;
this.line = line;
this.lineType = lineType;
this.lineAnchor = lineAnchor;
this.width = width;
+ this.textSizeType = textSizeType;
+ this.textSize = textSize;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
index 9a58ac07aa..81eb5dd888 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.trackselection;
import android.os.SystemClock;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
@@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index f2b4c7ed3e..71d2544784 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -20,6 +20,7 @@ import android.graphics.Point;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseArray;
@@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
@@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
@@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
@@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
index 2d457750e4..b37c8cad67 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.trackselection;
+import android.support.annotation.Nullable;
import java.util.Arrays;
/** An array of {@link TrackSelection}s. */
@@ -64,7 +65,7 @@ public final class TrackSelectionArray {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
index 4a2354e180..ce3230fa43 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream;
import android.net.Uri;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.io.IOException;
@@ -79,7 +80,7 @@ public interface DataSource {
*
* @return The {@link Uri} from which data is being read, or null if the source is not open.
*/
- Uri getUri();
+ @Nullable Uri getUri();
/**
* Closes the source.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
index a6b89a334d..ad7a9d0147 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -61,7 +61,7 @@ public final class DataSpec {
/**
* Body for a POST request, null otherwise.
*/
- public final byte[] postBody;
+ public final @Nullable byte[] postBody;
/**
* The absolute position of the data in the full stream.
*/
@@ -81,12 +81,12 @@ public final class DataSpec {
* A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
* {@link DataSpec} is not intended to be used in conjunction with a cache.
*/
- @Nullable public final String key;
+ public final @Nullable String key;
/**
* Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
* {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
*/
- @Flags public final int flags;
+ public final @Flags int flags;
/**
* Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
@@ -128,7 +128,8 @@ public final class DataSpec {
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
- public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) {
+ public DataSpec(
+ Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) {
this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
}
@@ -143,7 +144,12 @@ public final class DataSpec {
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
- public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key,
+ public DataSpec(
+ Uri uri,
+ long absoluteStreamPosition,
+ long position,
+ long length,
+ @Nullable String key,
@Flags int flags) {
this(uri, null, absoluteStreamPosition, position, length, key, flags);
}
@@ -162,7 +168,7 @@ public final class DataSpec {
*/
public DataSpec(
Uri uri,
- byte[] postBody,
+ @Nullable byte[] postBody,
long absoluteStreamPosition,
long position,
long length,
@@ -222,4 +228,13 @@ public final class DataSpec {
}
}
+ /**
+ * Returns a copy of this {@link DataSpec} with the specified Uri.
+ *
+ * @param uri The new source {@link Uri}.
+ * @return The copied {@link DataSpec} with the specified Uri.
+ */
+ public DataSpec withUri(Uri uri) {
+ return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java
index a36ccd11b1..729f7fe179 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream;
import android.net.Uri;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
@@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource {
}
@Override
- public Uri getUri() {
+ public @Nullable Uri getUri() {
return upstream.getUri();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
index 045fc25338..023567e7df 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
+import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource {
*/
public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
+ private static final String TAG = "CacheDataSource";
+
/**
* Flags controlling the cache's behavior.
*/
@@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource {
try {
key = CacheUtil.getKey(dataSpec);
uri = dataSpec.uri;
- actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
+ actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri);
flags = dataSpec.flags;
readPosition = dataSpec.position;
@@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource {
bytesRemaining -= bytesRead;
}
} else if (currentDataSpecLengthUnset) {
- setNoBytesRemainingAndMaybeStoreLength();
+ setBytesRemainingAndMaybeStoreLength(0);
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
closeCurrentSource();
openNextSource(false);
@@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource {
return bytesRead;
} catch (IOException e) {
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
- setNoBytesRemainingAndMaybeStoreLength();
+ setBytesRemainingAndMaybeStoreLength(0);
return C.RESULT_END_OF_INPUT;
}
handleBeforeThrow(e);
@@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource {
currentDataSource = nextDataSource;
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
long resolvedLength = nextDataSource.open(nextDataSpec);
-
- // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
- ContentMetadataMutations mutations = new ContentMetadataMutations();
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
- bytesRemaining = resolvedLength;
- ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
+ setBytesRemainingAndMaybeStoreLength(resolvedLength);
}
- if (isReadingFromUpstream()) {
- actualUri = currentDataSource.getUri();
- boolean isRedirected = !uri.equals(actualUri);
- if (isRedirected) {
- ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
- } else {
- ContentMetadataInternal.removeRedirectedUri(mutations);
- }
+ // TODO find a way to store length and redirected uri in one metadata mutation.
+ maybeUpdateActualUriFieldAndRedirectedUriMetadata();
+ }
+
+ private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() {
+ if (!isReadingFromUpstream()) {
+ return;
}
- if (isWritingToCache()) {
+ actualUri = currentDataSource.getUri();
+ maybeUpdateRedirectedUriMetadata();
+ }
+
+ private void maybeUpdateRedirectedUriMetadata() {
+ if (!isWritingToCache()) {
+ return;
+ }
+ ContentMetadataMutations mutations = new ContentMetadataMutations();
+ boolean isRedirected = !uri.equals(actualUri);
+ if (isRedirected) {
+ ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
+ } else {
+ ContentMetadataInternal.removeRedirectedUri(mutations);
+ }
+ try {
cache.applyContentMetadataMutations(key, mutations);
+ } catch (CacheException e) {
+ String message =
+ "Couldn't update redirected URI. "
+ + "This might cause relative URIs get resolved incorrectly.";
+ Log.w(TAG, message, e);
}
}
- private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
- bytesRemaining = 0;
- if (isWritingToCache()) {
- cache.setContentLength(key, readPosition);
- }
- }
-
- private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
+ private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) {
ContentMetadata contentMetadata = cache.getContentMetadata(key);
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
- return redirectedUri == null ? defaultUri : redirectedUri;
+ return redirectedUri == null ? uri : redirectedUri;
}
private static boolean isCausedByPositionOutOfRange(IOException e) {
@@ -447,6 +458,13 @@ public final class CacheDataSource implements DataSource {
return false;
}
+ private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException {
+ this.bytesRemaining = bytesRemaining;
+ if (isWritingToCache()) {
+ cache.setContentLength(key, readPosition + bytesRemaining);
+ }
+ }
+
private boolean isReadingFromUpstream() {
return !isReadingFromCache();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
index 7b0b459dd9..89835f31de 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream;
@@ -236,7 +237,7 @@ import java.util.TreeSet;
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
index 7b5fd2c598..3bcfac5053 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.upstream.cache;
-import android.util.Log;
import android.util.SparseArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
@@ -26,7 +25,6 @@ import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1;
- private static final String TAG = "CachedContentIndex";
-
private final HashMap keyToContent;
private final SparseArray idToKey;
private final AtomicFile atomicFile;
@@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec;
add(cachedContent);
hashCode += cachedContent.headerHashCode(version);
}
- if (input.readInt() != hashCode) {
+ int fileHashCode = input.readInt();
+ boolean isEOF = input.read() == -1;
+ if (fileHashCode != hashCode || !isEOF) {
return false;
}
- } catch (FileNotFoundException e) {
- return false;
} catch (IOException e) {
- Log.e(TAG, "Error reading cache content index file.", e);
return false;
} finally {
if (input != null) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
index b855befe00..aefb0f6852 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
new file mode 100644
index 0000000000..6fe76b9b2c
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLSurface;
+import android.opengl.GLES20;
+import android.os.Handler;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */
+@TargetApi(17)
+public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable {
+
+ /** Secure mode to be used by the EGL surface and context. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
+ public @interface SecureMode {}
+
+ /** No secure EGL surface and context required. */
+ public static final int SECURE_MODE_NONE = 0;
+ /** Creating a surfaceless, secured EGL context. */
+ public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;
+ /** Creating a secure surface backed by a pixel buffer. */
+ public static final int SECURE_MODE_PROTECTED_PBUFFER = 2;
+
+ private static final int[] EGL_CONFIG_ATTRIBUTES =
+ new int[] {
+ EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
+ EGL14.EGL_RED_SIZE, 8,
+ EGL14.EGL_GREEN_SIZE, 8,
+ EGL14.EGL_BLUE_SIZE, 8,
+ EGL14.EGL_ALPHA_SIZE, 8,
+ EGL14.EGL_DEPTH_SIZE, 0,
+ EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE,
+ EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
+ EGL14.EGL_NONE
+ };
+
+ private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
+
+ /** A runtime exception to be thrown if some EGL operations failed. */
+ public static final class GlException extends RuntimeException {
+ private GlException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final Handler handler;
+ private final int[] textureIdHolder;
+
+ private @Nullable EGLDisplay display;
+ private @Nullable EGLContext context;
+ private @Nullable EGLSurface surface;
+ private @Nullable SurfaceTexture texture;
+
+ /**
+ * @param handler The {@link Handler} that will be used to call {@link
+ * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that
+ * {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s
+ * looper.
+ */
+ public EGLSurfaceTexture(Handler handler) {
+ this.handler = handler;
+ textureIdHolder = new int[1];
+ }
+
+ /**
+ * Initializes required EGL parameters and creates the {@link SurfaceTexture}.
+ *
+ * @param secureMode The {@link SecureMode} to be used for EGL surface.
+ */
+ public void init(@SecureMode int secureMode) {
+ display = getDefaultDisplay();
+ EGLConfig config = chooseEGLConfig(display);
+ context = createEGLContext(display, config, secureMode);
+ surface = createEGLSurface(display, config, context, secureMode);
+ generateTextureIds(textureIdHolder);
+ texture = new SurfaceTexture(textureIdHolder[0]);
+ texture.setOnFrameAvailableListener(this);
+ }
+
+ /** Releases all allocated resources. */
+ @SuppressWarnings({"nullness:argument.type.incompatible"})
+ public void release() {
+ handler.removeCallbacks(this);
+ try {
+ if (texture != null) {
+ texture.release();
+ GLES20.glDeleteTextures(1, textureIdHolder, 0);
+ }
+ } finally {
+ if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) {
+ EGL14.eglDestroySurface(display, surface);
+ }
+ if (context != null) {
+ EGL14.eglDestroyContext(display, context);
+ }
+ display = null;
+ context = null;
+ surface = null;
+ texture = null;
+ }
+ }
+
+ /**
+ * Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}.
+ */
+ public SurfaceTexture getSurfaceTexture() {
+ return Assertions.checkNotNull(texture);
+ }
+
+ // SurfaceTexture.OnFrameAvailableListener
+
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ handler.post(this);
+ }
+
+ // Runnable
+
+ @Override
+ public void run() {
+ if (texture != null) {
+ texture.updateTexImage();
+ }
+ }
+
+ private static EGLDisplay getDefaultDisplay() {
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ if (display == null) {
+ throw new GlException("eglGetDisplay failed");
+ }
+
+ int[] version = new int[2];
+ boolean eglInitialized =
+ EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);
+ if (!eglInitialized) {
+ throw new GlException("eglInitialize failed");
+ }
+ return display;
+ }
+
+ private static EGLConfig chooseEGLConfig(EGLDisplay display) {
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfigs = new int[1];
+ boolean success =
+ EGL14.eglChooseConfig(
+ display,
+ EGL_CONFIG_ATTRIBUTES,
+ /* attrib_listOffset= */ 0,
+ configs,
+ /* configsOffset= */ 0,
+ /* config_size= */ 1,
+ numConfigs,
+ /* num_configOffset= */ 0);
+ if (!success || numConfigs[0] <= 0 || configs[0] == null) {
+ throw new GlException(
+ Util.formatInvariant(
+ /* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
+ success, numConfigs[0], configs[0]));
+ }
+
+ return configs[0];
+ }
+
+ private static EGLContext createEGLContext(
+ EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
+ int[] glAttributes;
+ if (secureMode == SECURE_MODE_NONE) {
+ glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
+ } else {
+ glAttributes =
+ new int[] {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION,
+ 2,
+ EGL_PROTECTED_CONTENT_EXT,
+ EGL14.EGL_TRUE,
+ EGL14.EGL_NONE
+ };
+ }
+ EGLContext context =
+ EGL14.eglCreateContext(
+ display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
+ if (context == null) {
+ throw new GlException("eglCreateContext failed");
+ }
+ return context;
+ }
+
+ private static EGLSurface createEGLSurface(
+ EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
+ EGLSurface surface;
+ if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
+ surface = EGL14.EGL_NO_SURFACE;
+ } else {
+ int[] pbufferAttributes;
+ if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) {
+ pbufferAttributes =
+ new int[] {
+ EGL14.EGL_WIDTH,
+ 1,
+ EGL14.EGL_HEIGHT,
+ 1,
+ EGL_PROTECTED_CONTENT_EXT,
+ EGL14.EGL_TRUE,
+ EGL14.EGL_NONE
+ };
+ } else {
+ pbufferAttributes =
+ new int[] {
+ EGL14.EGL_WIDTH, 1,
+ EGL14.EGL_HEIGHT, 1,
+ EGL14.EGL_NONE
+ };
+ }
+ surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
+ if (surface == null) {
+ throw new GlException("eglCreatePbufferSurface failed");
+ }
+ }
+
+ boolean eglMadeCurrent =
+ EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
+ if (!eglMadeCurrent) {
+ throw new GlException("eglMakeCurrent failed");
+ }
+ return surface;
+ }
+
+ private static void generateTextureIds(int[] textureIdHolder) {
+ GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
+ int errorCode = GLES20.glGetError();
+ if (errorCode != GLES20.GL_NO_ERROR) {
+ throw new GlException("glGenTextures failed. Error: " + Integer.toHexString(errorCode));
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java
index b08f4a31e3..0df39e103d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.util;
+import com.google.android.exoplayer2.C;
+
/**
* Holder for FLAC stream info.
*/
@@ -52,8 +54,29 @@ public final class FlacStreamInfo {
// Remaining 16 bytes is md5 value
}
- public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize,
- int sampleRate, int channels, int bitsPerSample, long totalSamples) {
+ /**
+ * Constructs a FlacStreamInfo given the parameters.
+ *
+ * @param minBlockSize Minimum block size of the FLAC stream.
+ * @param maxBlockSize Maximum block size of the FLAC stream.
+ * @param minFrameSize Minimum frame size of the FLAC stream.
+ * @param maxFrameSize Maximum frame size of the FLAC stream.
+ * @param sampleRate Sample rate of the FLAC stream.
+ * @param channels Number of channels of the FLAC stream.
+ * @param bitsPerSample Number of bits per sample of the FLAC stream.
+ * @param totalSamples Total samples of the FLAC stream.
+ * @see FLAC format
+ * METADATA_BLOCK_STREAMINFO
+ */
+ public FlacStreamInfo(
+ int minBlockSize,
+ int maxBlockSize,
+ int minFrameSize,
+ int maxFrameSize,
+ int sampleRate,
+ int channels,
+ int bitsPerSample,
+ long totalSamples) {
this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize;
@@ -64,16 +87,43 @@ public final class FlacStreamInfo {
this.totalSamples = totalSamples;
}
+ /** Returns the maximum size for a decoded frame from the FLAC stream. */
public int maxDecodedFrameSize() {
return maxBlockSize * channels * (bitsPerSample / 8);
}
+ /** Returns the bit-rate of the FLAC stream. */
public int bitRate() {
return bitsPerSample * sampleRate;
}
+ /** Returns the duration of the FLAC stream in microseconds. */
public long durationUs() {
return (totalSamples * 1000000L) / sampleRate;
}
+ /**
+ * Returns the sample index for the sample at given position.
+ *
+ * @param timeUs Time position in microseconds in the FLAC stream.
+ * @return The sample index for the sample at given position.
+ */
+ public long getSampleIndex(long timeUs) {
+ long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
+ return Util.constrainValue(sampleIndex, 0, totalSamples - 1);
+ }
+
+ /** Returns the approximate number of bytes per frame for the current FLAC stream. */
+ public long getApproxBytesPerFrame() {
+ long approxBytesPerFrame;
+ if (maxFrameSize > 0) {
+ approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;
+ } else {
+ // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the
+ // default value for FLAC block-size, which is 4096.
+ long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096;
+ approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64;
+ }
+ return approxBytesPerFrame;
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java
index 6592273d03..071ebf2084 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java
@@ -143,6 +143,26 @@ public final class UriUtil {
}
}
+ /**
+ * Removes query parameter from an Uri, if present.
+ *
+ * @param uri The uri.
+ * @param queryParameterName The name of the query parameter.
+ * @return The uri without the query parameter.
+ */
+ public static Uri removeQueryParameter(Uri uri, String queryParameterName) {
+ Uri.Builder builder = uri.buildUpon();
+ builder.clearQuery();
+ for (String key : uri.getQueryParameterNames()) {
+ if (!key.equals(queryParameterName)) {
+ for (String value : uri.getQueryParameters(key)) {
+ builder.appendQueryParameter(key, value);
+ }
+ }
+ }
+ return builder.build();
+ }
+
/**
* Removes dot segments from the path of a URI.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java
index a983a0a6a3..faedaaf273 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java
@@ -17,10 +17,10 @@ package com.google.android.exoplayer2.video;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
-
import java.util.Arrays;
/**
@@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable {
// Parcelable implementation.
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
index fc31a33097..2f41831a5e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
@@ -15,29 +15,29 @@
*/
package com.google.android.exoplayer2.video;
+import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE;
+import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER;
+import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT;
+
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
-import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.opengl.EGL14;
-import android.opengl.EGLConfig;
-import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
-import android.opengl.EGLSurface;
-import android.opengl.GLES20;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Message;
-import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.EGLSurfaceTexture;
+import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode;
import com.google.android.exoplayer2.util.Util;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import javax.microedition.khronos.egl.EGL10;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A dummy {@link Surface}.
@@ -50,16 +50,6 @@ public final class DummySurface extends Surface {
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
- private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
- private @interface SecureMode {}
-
- private static final int SECURE_MODE_NONE = 0;
- private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;
- private static final int SECURE_MODE_PROTECTED_PBUFFER = 2;
-
/**
* Whether the surface is secure.
*/
@@ -161,32 +151,25 @@ public final class DummySurface extends Surface {
: SECURE_MODE_PROTECTED_PBUFFER;
}
- private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
- Callback {
+ private static class DummySurfaceThread extends HandlerThread implements Callback {
private static final int MSG_INIT = 1;
- private static final int MSG_UPDATE_TEXTURE = 2;
- private static final int MSG_RELEASE = 3;
+ private static final int MSG_RELEASE = 2;
- private final int[] textureIdHolder;
- private EGLDisplay display;
- private EGLContext context;
- private EGLSurface pbuffer;
- private Handler handler;
- private SurfaceTexture surfaceTexture;
-
- private Error initError;
- private RuntimeException initException;
- private DummySurface surface;
+ private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure;
+ private @MonotonicNonNull Handler handler;
+ private @Nullable Error initError;
+ private @Nullable RuntimeException initException;
+ private @Nullable DummySurface surface;
public DummySurfaceThread() {
super("dummySurface");
- textureIdHolder = new int[1];
}
public DummySurface init(@SecureMode int secureMode) {
start();
- handler = new Handler(getLooper(), this);
+ handler = new Handler(getLooper(), /* callback= */ this);
+ eglSurfaceTexure = new EGLSurfaceTexture(handler);
boolean wasInterrupted = false;
synchronized (this) {
handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();
@@ -207,19 +190,15 @@ public final class DummySurface extends Surface {
} else if (initError != null) {
throw initError;
} else {
- return surface;
+ return Assertions.checkNotNull(surface);
}
}
public void release() {
+ Assertions.checkNotNull(handler);
handler.sendEmptyMessage(MSG_RELEASE);
}
- @Override
- public void onFrameAvailable(SurfaceTexture surfaceTexture) {
- handler.sendEmptyMessage(MSG_UPDATE_TEXTURE);
- }
-
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
@@ -238,9 +217,6 @@ public final class DummySurface extends Surface {
}
}
return true;
- case MSG_UPDATE_TEXTURE:
- surfaceTexture.updateTexImage();
- return true;
case MSG_RELEASE:
try {
releaseInternal();
@@ -256,103 +232,16 @@ public final class DummySurface extends Surface {
}
private void initInternal(@SecureMode int secureMode) {
- display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
- Assertions.checkState(display != null, "eglGetDisplay failed");
-
- int[] version = new int[2];
- boolean eglInitialized = EGL14.eglInitialize(display, version, 0, version, 1);
- Assertions.checkState(eglInitialized, "eglInitialize failed");
-
- int[] eglAttributes =
- new int[] {
- EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
- EGL14.EGL_RED_SIZE, 8,
- EGL14.EGL_GREEN_SIZE, 8,
- EGL14.EGL_BLUE_SIZE, 8,
- EGL14.EGL_ALPHA_SIZE, 8,
- EGL14.EGL_DEPTH_SIZE, 0,
- EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE,
- EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
- EGL14.EGL_NONE
- };
- EGLConfig[] configs = new EGLConfig[1];
- int[] numConfigs = new int[1];
- boolean eglChooseConfigSuccess =
- EGL14.eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, numConfigs, 0);
- Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null,
- "eglChooseConfig failed");
-
- EGLConfig config = configs[0];
- int[] glAttributes;
- if (secureMode == SECURE_MODE_NONE) {
- glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
- } else {
- glAttributes =
- new int[] {
- EGL14.EGL_CONTEXT_CLIENT_VERSION,
- 2,
- EGL_PROTECTED_CONTENT_EXT,
- EGL14.EGL_TRUE,
- EGL14.EGL_NONE
- };
- }
- context =
- EGL14.eglCreateContext(
- display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
- Assertions.checkState(context != null, "eglCreateContext failed");
-
- EGLSurface surface;
- if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
- surface = EGL14.EGL_NO_SURFACE;
- } else {
- int[] pbufferAttributes;
- if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) {
- pbufferAttributes =
- new int[] {
- EGL14.EGL_WIDTH,
- 1,
- EGL14.EGL_HEIGHT,
- 1,
- EGL_PROTECTED_CONTENT_EXT,
- EGL14.EGL_TRUE,
- EGL14.EGL_NONE
- };
- } else {
- pbufferAttributes = new int[] {EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE};
- }
- pbuffer = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, 0);
- Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed");
- surface = pbuffer;
- }
-
- boolean eglMadeCurrent = EGL14.eglMakeCurrent(display, surface, surface, context);
- Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed");
-
- GLES20.glGenTextures(1, textureIdHolder, 0);
- surfaceTexture = new SurfaceTexture(textureIdHolder[0]);
- surfaceTexture.setOnFrameAvailableListener(this);
- this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE);
+ Assertions.checkNotNull(eglSurfaceTexure);
+ eglSurfaceTexure.init(secureMode);
+ this.surface =
+ new DummySurface(
+ this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
}
private void releaseInternal() {
- try {
- if (surfaceTexture != null) {
- surfaceTexture.release();
- GLES20.glDeleteTextures(1, textureIdHolder, 0);
- }
- } finally {
- if (pbuffer != null) {
- EGL14.eglDestroySurface(display, pbuffer);
- }
- if (context != null) {
- EGL14.eglDestroyContext(display, context);
- }
- pbuffer = null;
- context = null;
- display = null;
- surface = null;
- surfaceTexture = null;
- }
+ Assertions.checkNotNull(eglSurfaceTexure);
+ eglSurfaceTexure.release();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index 34a3eb7284..579f7c45f4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4006,
// https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104.
+ // https://github.com/google/ExoPlayer/issues/4134.
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|| "mido".equals(Util.DEVICE) // Redmi Note 4
@@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|| "F3311".equals(Util.DEVICE) // Sony Xperia E5
|| "M5c".equals(Util.DEVICE) // Meizu M5C
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
- || "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note
+ || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note
+ || "woods_f".equals(Util.MODEL)) // Moto E (4)
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
index ed91f6651c..0df854cddb 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -51,6 +51,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -1812,6 +1813,88 @@ public final class ExoPlayerTest {
assertThat(target3.windowIndex).isEqualTo(2);
}
+ @Test
+ public void testCancelMessageBeforeDelivery() throws Exception {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
+ final AtomicReference message = new AtomicReference<>();
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder("testCancelMessage")
+ .pause()
+ .waitForPlaybackState(Player.STATE_BUFFERING)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ message.set(
+ player.createMessage(target).setPosition(/* positionMs= */ 50).send());
+ }
+ })
+ // Play a bit to ensure message arrived in internal player.
+ .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 30)
+ .executeRunnable(
+ new Runnable() {
+ @Override
+ public void run() {
+ message.get().cancel();
+ }
+ })
+ .play()
+ .build();
+ new Builder()
+ .setTimeline(timeline)
+ .setActionSchedule(actionSchedule)
+ .build()
+ .start()
+ .blockUntilEnded(TIMEOUT_MS);
+ assertThat(message.get().isCanceled()).isTrue();
+ assertThat(target.messageCount).isEqualTo(0);
+ }
+
+ @Test
+ public void testCancelRepeatedMessageAfterDelivery() throws Exception {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
+ final AtomicReference message = new AtomicReference<>();
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder("testCancelMessage")
+ .pause()
+ .waitForPlaybackState(Player.STATE_BUFFERING)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ message.set(
+ player
+ .createMessage(target)
+ .setPosition(/* positionMs= */ 50)
+ .setDeleteAfterDelivery(/* deleteAfterDelivery= */ false)
+ .send());
+ }
+ })
+ // Play until the message has been delivered.
+ .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 51)
+ // Seek back, cancel the message, and play past the same position again.
+ .seek(/* positionMs= */ 0)
+ .executeRunnable(
+ new Runnable() {
+ @Override
+ public void run() {
+ message.get().cancel();
+ }
+ })
+ .play()
+ .build();
+ new Builder()
+ .setTimeline(timeline)
+ .setActionSchedule(actionSchedule)
+ .build()
+ .start()
+ .blockUntilEnded(TIMEOUT_MS);
+ assertThat(message.get().isCanceled()).isTrue();
+ assertThat(target.messageCount).isEqualTo(1);
+ }
+
@Test
public void testSetAndSwitchSurface() throws Exception {
final List rendererMessages = new ArrayList<>();
@@ -1934,8 +2017,10 @@ public final class ExoPlayerTest {
@Override
public void handleMessage(SimpleExoPlayer player, int messageType, Object message) {
- windowIndex = player.getCurrentWindowIndex();
- positionMs = player.getCurrentPosition();
+ if (player != null) {
+ windowIndex = player.getCurrentWindowIndex();
+ positionMs = player.getCurrentPosition();
+ }
messageCount++;
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java
index 829fa5a2b8..623506ad0d 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java
@@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest {
}
@Override
- public boolean equals(Object other) {
+ public boolean equals(@Nullable Object other) {
if (!(other instanceof EventWindowAndPeriodId)) {
return false;
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java
index a52867e1b2..82c62ecb3e 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java
@@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer2.util;
+import static com.google.android.exoplayer2.util.UriUtil.removeQueryParameter;
import static com.google.android.exoplayer2.util.UriUtil.resolve;
import static com.google.common.truth.Truth.assertThat;
+import android.net.Uri;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -104,4 +106,36 @@ public final class UriUtilTest {
assertThat(resolve("a:b", "../c")).isEqualTo("a:c");
}
+ @Test
+ public void removeOnlyQueryParameter() {
+ Uri uri = Uri.parse("http://uri?query=value");
+ assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri");
+ }
+
+ @Test
+ public void removeFirstQueryParameter() {
+ Uri uri = Uri.parse("http://uri?query=value&second=value2");
+ assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?second=value2");
+ }
+
+ @Test
+ public void removeMiddleQueryParameter() {
+ Uri uri = Uri.parse("http://uri?first=value1&query=value&last=value2");
+ assertThat(removeQueryParameter(uri, "query").toString())
+ .isEqualTo("http://uri?first=value1&last=value2");
+ }
+
+ @Test
+ public void removeLastQueryParameter() {
+ Uri uri = Uri.parse("http://uri?first=value1&query=value");
+ assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?first=value1");
+ }
+
+ @Test
+ public void removeNonExistentQueryParameter() {
+ Uri uri = Uri.parse("http://uri");
+ assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri");
+ uri = Uri.parse("http://uri?query=value");
+ assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri?query=value");
+ }
}
diff --git a/library/dash/build.gradle b/library/dash/build.gradle
index 81b247d047..867b288498 100644
--- a/library/dash/build.gradle
+++ b/library/dash/build.gradle
@@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true
// }
}
-
- lintOptions {
- lintConfig file("../../checker-framework-lint.xml")
- }
}
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java
index 8a6069e477..bd19ff6587 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java
@@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper {
manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri);
}
+ /** Returns the DASH manifest. Must not be called until after preparation completes. */
+ public DashManifest getManifest() {
+ Assertions.checkNotNull(manifest);
+ return manifest;
+ }
+
@Override
public int getPeriodCount() {
Assertions.checkNotNull(manifest);
diff --git a/library/hls/build.gradle b/library/hls/build.gradle
index c599931a68..6aeb33e195 100644
--- a/library/hls/build.gradle
+++ b/library/hls/build.gradle
@@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true
// }
}
-
- lintOptions {
- lintConfig file("../../checker-framework-lint.xml")
- }
}
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils-robolectric')
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
index 9a02bd785a..0fb1b6a969 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -198,24 +198,24 @@ import java.util.List;
/**
* Returns the next chunk to load.
- *
- * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has
- * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but
- * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
+ *
+ *
If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream
+ * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available
+ * but the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
* contain the {@link HlsUrl} that refers to the playlist that needs refreshing.
*
* @param previous The most recently loaded media chunk.
- * @param playbackPositionUs The current playback position in microseconds. If playback of the
- * period to which this chunk source belongs has not yet started, the value will be the
- * starting position in the period minus the duration of any media in previous periods still
- * to be played.
- * @param loadPositionUs The current load position in microseconds. If {@code previous} is null,
- * this is the starting position from which chunks should be provided. Else it's equal to
- * {@code previous.endTimeUs}.
+ * @param playbackPositionUs The current playback position relative to the period start in
+ * microseconds. If playback of the period to which this chunk source belongs has not yet
+ * started, the value will be the starting position in the period minus the duration of any
+ * media in previous periods still to be played.
+ * @param loadPositionUs The current load position relative to the period start in microseconds.
+ * If {@code previous} is null, this is the starting position from which chunks should be
+ * provided. Else it's equal to {@code previous.endTimeUs}.
* @param out A holder to populate.
*/
- public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs,
- HlsChunkHolder out) {
+ public void getNextChunk(
+ HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) {
int oldVariantIndex = previous == null ? C.INDEX_UNSET
: trackGroup.indexOf(previous.trackFormat);
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
@@ -261,12 +261,13 @@ import java.util.List;
// If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
} else {
- // The playlist start time is subtracted from the target position because the segment start
- // times are relative to the start of the playlist, but the target position is not.
+ long positionOfPlaylistInPeriodUs =
+ mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
+ long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs;
chunkMediaSequence =
Util.binarySearchFloor(
mediaPlaylist.segments,
- /* value= */ targetPositionUs - mediaPlaylist.startTimeUs,
+ /* value= */ targetPositionInPlaylistUs,
/* inclusive= */ true,
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ mediaPlaylist.mediaSequence;
@@ -330,9 +331,9 @@ import java.util.List;
}
// Compute start time of the next chunk.
- long offsetFromInitialStartTimeUs =
+ long positionOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
- long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs;
+ long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
@@ -352,8 +353,8 @@ import java.util.List;
muxedCaptionFormats,
trackSelection.getSelectionReason(),
trackSelection.getSelectionData(),
- startTimeUs,
- startTimeUs + segment.durationUs,
+ segmentStartTimeInPeriodUs,
+ segmentStartTimeInPeriodUs + segment.durationUs,
chunkMediaSequence,
discontinuitySequence,
segment.hasGapTag,
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
index 5d4d953372..f43d119018 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
/**
@@ -36,6 +37,11 @@ import java.io.IOException;
sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
}
+ public void bindSampleQueue() {
+ Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);
+ sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
+ }
+
public void unbindSampleQueue() {
if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
@@ -48,12 +54,11 @@ import java.io.IOException;
@Override
public boolean isReady() {
return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
- || (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex));
+ || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
}
@Override
public void maybeThrowError() throws IOException {
- maybeMapToSampleQueue();
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {
throw new SampleQueueMappingException(
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
@@ -63,22 +68,21 @@ import java.io.IOException;
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
- return maybeMapToSampleQueue()
+ return hasValidSampleQueueIndex()
? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat)
: C.RESULT_NOTHING_READ;
}
@Override
public int skipData(long positionUs) {
- return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0;
+ return hasValidSampleQueueIndex()
+ ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)
+ : 0;
}
// Internal methods.
- private boolean maybeMapToSampleQueue() {
- if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
- sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
- }
+ private boolean hasValidSampleQueueIndex() {
return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
index 0de4faa9c0..705320bdad 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -102,6 +102,7 @@ import java.util.Arrays;
private final Runnable maybeFinishPrepareRunnable;
private final Runnable onTracksEndedRunnable;
private final Handler handler;
+ private final ArrayList hlsSampleStreams;
private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds;
@@ -166,6 +167,7 @@ import java.util.Arrays;
sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>();
+ hlsSampleStreams = new ArrayList<>();
maybeFinishPrepareRunnable =
new Runnable() {
@Override
@@ -219,9 +221,6 @@ import java.util.Arrays;
}
public int bindSampleQueueToSampleStream(int trackGroupIndex) {
- if (trackGroupToSampleQueueIndex == null) {
- return SAMPLE_QUEUE_INDEX_PENDING;
- }
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
if (sampleQueueIndex == C.INDEX_UNSET) {
return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET
@@ -295,6 +294,9 @@ import java.util.Arrays;
}
streams[i] = new HlsSampleStream(this, trackGroupIndex);
streamResetFlags[i] = true;
+ if (trackGroupToSampleQueueIndex != null) {
+ ((HlsSampleStream) streams[i]).bindSampleQueue();
+ }
// If there's still a chance of avoiding a seek, try and seek within the sample queue.
if (sampleQueuesBuilt && !seekRequired) {
SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]];
@@ -360,6 +362,7 @@ import java.util.Arrays;
}
}
+ updateSampleStreams(streams);
seenFirstTrackSelection = true;
return seekRequired;
}
@@ -411,6 +414,7 @@ import java.util.Arrays;
loader.release(this);
handler.removeCallbacksAndMessages(null);
released = true;
+ hlsSampleStreams.clear();
}
@Override
@@ -750,6 +754,15 @@ import java.util.Arrays;
// Internal methods.
+ private void updateSampleStreams(SampleStream[] streams) {
+ hlsSampleStreams.clear();
+ for (SampleStream stream : streams) {
+ if (stream != null) {
+ hlsSampleStreams.add((HlsSampleStream) stream);
+ }
+ }
+ }
+
private boolean finishedReadingChunk(HlsMediaChunk chunk) {
int chunkUid = chunk.uid;
int sampleQueueCount = sampleQueues.length;
@@ -807,6 +820,9 @@ import java.util.Arrays;
}
}
}
+ for (HlsSampleStream sampleStream : hlsSampleStreams) {
+ sampleStream.bindSampleQueue();
+ }
}
/**
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java
index 2d430d2c79..9c9cb532a6 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.hls;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.TrackGroup;
import java.io.IOException;
@@ -23,7 +24,7 @@ import java.io.IOException;
public final class SampleQueueMappingException extends IOException {
/** @param mimeType The mime type of the track group whose mapping failed. */
- public SampleQueueMappingException(String mimeType) {
+ public SampleQueueMappingException(@Nullable String mimeType) {
super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + ".");
}
}
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java
index 773aec49ee..37aa181970 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java
@@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper {
playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri);
}
+ /** Returns the HLS playlist. Must not be called until after preparation completes. */
+ public HlsPlaylist getPlaylist() {
+ Assertions.checkNotNull(playlist);
+ return playlist;
+ }
+
@Override
public int getPeriodCount() {
Assertions.checkNotNull(playlist);
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
index f905def54b..5ac6f37550 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/
public final long startOffsetUs;
/**
- * The start time of the playlist in playback timebase in microseconds.
+ * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.
+ * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
+ * playlist.
*/
public final long startTimeUs;
/**
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
index 4ed6aa1656..9986f5b65b 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
@@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback
- * A DefaultTimeBar can be customized by setting attributes, as outlined below.
+ *
+ * A DefaultTimeBar can be customized by setting attributes, as outlined below.
*
*
Attributes
+ *
* The following attributes can be set on a DefaultTimeBar when used in a layout XML file:
+ *
*
+ *
*
* - {@code bar_height} - Dimension for the height of the time bar.
*
- * - Default: {@link #DEFAULT_BAR_HEIGHT_DP}
+ * - Default: {@link #DEFAULT_BAR_HEIGHT_DP}
*
- *
* - {@code touch_target_height} - Dimension for the height of the area in which touch
* interactions with the time bar are handled. If no height is specified, this also determines
* the height of the view.
*
- * - Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
+ * - Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
*
- *
* - {@code ad_marker_width} - Dimension for the width of any ad markers shown on the
* bar. Ad markers are superimposed on the time bar to show the times at which ads will play.
*
- * - Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
+ * - Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
*
- *
* - {@code scrubber_enabled_size} - Dimension for the diameter of the circular scrubber
* handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle
* should be shown.
*
- * - Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
+ * - Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
*
- *
* - {@code scrubber_disabled_size} - Dimension for the diameter of the circular scrubber
* handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.
*
- * - Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
+ * - Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
*
- *
* - {@code scrubber_dragged_size} - Dimension for the diameter of the circular scrubber
* handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.
*
- * - Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
+ * - Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
*
- *
* - {@code scrubber_drawable} - Optional reference to a drawable to draw for the
* scrubber handle. If set, this overrides the default behavior, which is to draw a circle for
* the scrubber handle.
- *
* - {@code played_color} - Color for the portion of the time bar representing media
* before the current playback position.
*
- * - Default: {@link #DEFAULT_PLAYED_COLOR}
+ * - Corresponding method: {@link #setPlayedColor(int)}
+ *
- Default: {@link #DEFAULT_PLAYED_COLOR}
*
- *
* - {@code scrubber_color} - Color for the scrubber handle.
*
- * - Default: see {@link #getDefaultScrubberColor(int)}
+ * - Corresponding method: {@link #setScrubberColor(int)}
+ *
- Default: see {@link #getDefaultScrubberColor(int)}
*
- *
* - {@code buffered_color} - Color for the portion of the time bar after the current
* played position up to the current buffered position.
*
- * - Default: see {@link #getDefaultBufferedColor(int)}
+ * - Corresponding method: {@link #setBufferedColor(int)}
+ *
- Default: see {@link #getDefaultBufferedColor(int)}
*
- *
* - {@code unplayed_color} - Color for the portion of the time bar after the current
* buffered position.
*
- * - Default: see {@link #getDefaultUnplayedColor(int)}
+ * - Corresponding method: {@link #setUnplayedColor(int)}
+ *
- Default: see {@link #getDefaultUnplayedColor(int)}
*
- *
* - {@code ad_marker_color} - Color for unplayed ad markers.
*
- * - Default: {@link #DEFAULT_AD_MARKER_COLOR}
+ * - Corresponding method: {@link #setAdMarkerColor(int)}
+ *
- Default: {@link #DEFAULT_AD_MARKER_COLOR}
*
- *
* - {@code played_ad_marker_color} - Color for played ad markers.
*
- * - Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
+ * - Corresponding method: {@link #setPlayedAdMarkerColor(int)}
+ *
- Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
*
- *
*
*/
public class DefaultTimeBar extends View implements TimeBar {
@@ -324,6 +321,72 @@ public class DefaultTimeBar extends View implements TimeBar {
}
}
+ /**
+ * Sets the color for the portion of the time bar representing media before the playback position.
+ *
+ * @param playedColor The color for the portion of the time bar representing media before the
+ * playback position.
+ */
+ public void setPlayedColor(@ColorInt int playedColor) {
+ playedPaint.setColor(playedColor);
+ invalidate(seekBounds);
+ }
+
+ /**
+ * Sets the color for the scrubber handle.
+ *
+ * @param scrubberColor The color for the scrubber handle.
+ */
+ public void setScrubberColor(@ColorInt int scrubberColor) {
+ scrubberPaint.setColor(scrubberColor);
+ invalidate(seekBounds);
+ }
+
+ /**
+ * Sets the color for the portion of the time bar after the current played position up to the
+ * current buffered position.
+ *
+ * @param bufferedColor The color for the portion of the time bar after the current played
+ * position up to the current buffered position.
+ */
+ public void setBufferedColor(@ColorInt int bufferedColor) {
+ bufferedPaint.setColor(bufferedColor);
+ invalidate(seekBounds);
+ }
+
+ /**
+ * Sets the color for the portion of the time bar after the current played position.
+ *
+ * @param unplayedColor The color for the portion of the time bar after the current played
+ * position.
+ */
+ public void setUnplayedColor(@ColorInt int unplayedColor) {
+ unplayedPaint.setColor(unplayedColor);
+ invalidate(seekBounds);
+ }
+
+ /**
+ * Sets the color for unplayed ad markers.
+ *
+ * @param adMarkerColor The color for unplayed ad markers.
+ */
+ public void setAdMarkerColor(@ColorInt int adMarkerColor) {
+ adMarkerPaint.setColor(adMarkerColor);
+ invalidate(seekBounds);
+ }
+
+ /**
+ * Sets the color for played ad markers.
+ *
+ * @param playedAdMarkerColor The color for played ad markers.
+ */
+ public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) {
+ playedAdMarkerPaint.setColor(playedAdMarkerColor);
+ invalidate(seekBounds);
+ }
+
+ // TimeBar implementation.
+
@Override
public void addListener(OnScrubListener listener) {
listeners.add(listener);
@@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar {
update();
}
+ // View methods.
+
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
@@ -408,8 +473,8 @@ public class DefaultTimeBar extends View implements TimeBar {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isInSeekBar(x, y)) {
- startScrubbing();
positionScrubber(x);
+ startScrubbing();
scrubPosition = getScrubberPosition();
update();
invalidate();
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java
index 4c258c748f..19051ba932 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java
@@ -50,6 +50,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* A notification manager to start, update and cancel a media style notification reflecting the
@@ -205,7 +206,9 @@ public class PlayerNotificationManager {
new Runnable() {
@Override
public void run() {
- if (notificationTag == currentNotificationTag && isNotificationStarted) {
+ if (player != null
+ && notificationTag == currentNotificationTag
+ && isNotificationStarted) {
updateNotification(bitmap);
}
}
@@ -260,7 +263,7 @@ public class PlayerNotificationManager {
private final String channelId;
private final int notificationId;
private final MediaDescriptionAdapter mediaDescriptionAdapter;
- private final CustomActionReceiver customActionReceiver;
+ private final @Nullable CustomActionReceiver customActionReceiver;
private final Handler mainHandler;
private final NotificationManagerCompat notificationManager;
private final IntentFilter intentFilter;
@@ -269,12 +272,12 @@ public class PlayerNotificationManager {
private final Map playbackActions;
private final Map customActions;
- private Player player;
+ private @Nullable Player player;
private ControlDispatcher controlDispatcher;
private boolean isNotificationStarted;
private int currentNotificationTag;
- private NotificationListener notificationListener;
- private MediaSessionCompat.Token mediaSessionToken;
+ private @Nullable NotificationListener notificationListener;
+ private @Nullable MediaSessionCompat.Token mediaSessionToken;
private boolean useNavigationActions;
private boolean usePlayPauseActions;
private @Nullable String stopAction;
@@ -365,6 +368,20 @@ public class PlayerNotificationManager {
playerListener = new PlayerListener();
notificationBroadcastReceiver = new NotificationBroadcastReceiver();
intentFilter = new IntentFilter();
+ useNavigationActions = true;
+ usePlayPauseActions = true;
+ ongoing = true;
+ colorized = true;
+ useChronometer = true;
+ color = Color.TRANSPARENT;
+ smallIconResourceId = R.drawable.exo_notification_small_icon;
+ defaults = 0;
+ priority = NotificationCompat.PRIORITY_LOW;
+ fastForwardMs = DEFAULT_FAST_FORWARD_MS;
+ rewindMs = DEFAULT_REWIND_MS;
+ stopAction = ACTION_STOP;
+ badgeIconType = NotificationCompat.BADGE_ICON_SMALL;
+ visibility = NotificationCompat.VISIBILITY_PUBLIC;
// initialize actions
playbackActions = createPlaybackActions(context);
@@ -378,22 +395,7 @@ public class PlayerNotificationManager {
for (String action : customActions.keySet()) {
intentFilter.addAction(action);
}
-
- setStopAction(ACTION_STOP);
-
- useNavigationActions = true;
- usePlayPauseActions = true;
- ongoing = true;
- colorized = true;
- useChronometer = true;
- color = Color.TRANSPARENT;
- smallIconResourceId = R.drawable.exo_notification_small_icon;
- defaults = 0;
- priority = NotificationCompat.PRIORITY_LOW;
- fastForwardMs = DEFAULT_FAST_FORWARD_MS;
- rewindMs = DEFAULT_REWIND_MS;
- setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);
- setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+ stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
}
/**
@@ -512,10 +514,9 @@ public class PlayerNotificationManager {
}
this.stopAction = stopAction;
if (ACTION_STOP.equals(stopAction)) {
- stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent;
+ stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
} else if (stopAction != null) {
- Assertions.checkArgument(customActions.containsKey(stopAction));
- stopPendingIntent = customActions.get(stopAction).actionIntent;
+ stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent;
} else {
stopPendingIntent = null;
}
@@ -698,25 +699,28 @@ public class PlayerNotificationManager {
maybeUpdateNotification();
}
- private Notification updateNotification(Bitmap bitmap) {
+ @RequiresNonNull("player")
+ private Notification updateNotification(@Nullable Bitmap bitmap) {
Notification notification = createNotification(player, bitmap);
notificationManager.notify(notificationId, notification);
return notification;
}
private void startOrUpdateNotification() {
- Notification notification = updateNotification(null);
- if (!isNotificationStarted) {
- isNotificationStarted = true;
- context.registerReceiver(notificationBroadcastReceiver, intentFilter);
- if (notificationListener != null) {
- notificationListener.onNotificationStarted(notificationId, notification);
+ if (player != null) {
+ Notification notification = updateNotification(null);
+ if (!isNotificationStarted) {
+ isNotificationStarted = true;
+ context.registerReceiver(notificationBroadcastReceiver, intentFilter);
+ if (notificationListener != null) {
+ notificationListener.onNotificationStarted(notificationId, notification);
+ }
}
}
}
private void maybeUpdateNotification() {
- if (isNotificationStarted) {
+ if (isNotificationStarted && player != null) {
updateNotification(null);
}
}
@@ -732,64 +736,6 @@ public class PlayerNotificationManager {
}
}
- private Map createPlaybackActions(Context context) {
- Map actions = new HashMap<>();
- Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName());
- actions.put(
- ACTION_PLAY,
- new NotificationCompat.Action(
- R.drawable.exo_notification_play,
- context.getString(R.string.exo_controls_play_description),
- PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName());
- actions.put(
- ACTION_PAUSE,
- new NotificationCompat.Action(
- R.drawable.exo_notification_pause,
- context.getString(R.string.exo_controls_pause_description),
- PendingIntent.getBroadcast(
- context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName());
- actions.put(
- ACTION_STOP,
- new NotificationCompat.Action(
- R.drawable.exo_notification_stop,
- context.getString(R.string.exo_controls_stop_description),
- PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName());
- actions.put(
- ACTION_REWIND,
- new NotificationCompat.Action(
- R.drawable.exo_notification_rewind,
- context.getString(R.string.exo_controls_rewind_description),
- PendingIntent.getBroadcast(
- context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName());
- actions.put(
- ACTION_FAST_FORWARD,
- new NotificationCompat.Action(
- R.drawable.exo_notification_fastforward,
- context.getString(R.string.exo_controls_fastforward_description),
- PendingIntent.getBroadcast(
- context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName());
- actions.put(
- ACTION_PREVIOUS,
- new NotificationCompat.Action(
- R.drawable.exo_notification_previous,
- context.getString(R.string.exo_controls_previous_description),
- PendingIntent.getBroadcast(
- context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName());
- actions.put(
- ACTION_NEXT,
- new NotificationCompat.Action(
- R.drawable.exo_notification_next,
- context.getString(R.string.exo_controls_next_description),
- PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
- return actions;
- }
-
/**
* Creates the notification given the current player state.
*
@@ -821,7 +767,7 @@ public class PlayerNotificationManager {
// Configure stop action (eg. when user dismisses the notification when !isOngoing).
boolean useStopAction = stopAction != null && !isPlayingAd;
mediaStyle.setShowCancelButton(useStopAction);
- if (useStopAction) {
+ if (useStopAction && stopPendingIntent != null) {
builder.setDeleteIntent(stopPendingIntent);
mediaStyle.setCancelButtonIntent(stopPendingIntent);
}
@@ -905,7 +851,7 @@ public class PlayerNotificationManager {
if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) {
stringActions.add(ACTION_NEXT);
}
- if (!customActions.isEmpty()) {
+ if (customActionReceiver != null) {
stringActions.addAll(customActionReceiver.getCustomActions(player));
}
if (ACTION_STOP.equals(stopAction)) {
@@ -932,6 +878,64 @@ public class PlayerNotificationManager {
return new int[] {actionIndex};
}
+ private static Map createPlaybackActions(Context context) {
+ Map actions = new HashMap<>();
+ Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_PLAY,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_play,
+ context.getString(R.string.exo_controls_play_description),
+ PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_PAUSE,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_pause,
+ context.getString(R.string.exo_controls_pause_description),
+ PendingIntent.getBroadcast(
+ context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_STOP,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_stop,
+ context.getString(R.string.exo_controls_stop_description),
+ PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_REWIND,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_rewind,
+ context.getString(R.string.exo_controls_rewind_description),
+ PendingIntent.getBroadcast(
+ context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_FAST_FORWARD,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_fastforward,
+ context.getString(R.string.exo_controls_fastforward_description),
+ PendingIntent.getBroadcast(
+ context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_PREVIOUS,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_previous,
+ context.getString(R.string.exo_controls_previous_description),
+ PendingIntent.getBroadcast(
+ context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName());
+ actions.put(
+ ACTION_NEXT,
+ new NotificationCompat.Action(
+ R.drawable.exo_notification_next,
+ context.getString(R.string.exo_controls_next_description),
+ PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
+ return actions;
+ }
+
private class PlayerListener extends Player.DefaultEventListener {
@Override
@@ -946,7 +950,7 @@ public class PlayerNotificationManager {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
- if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification();
@@ -954,7 +958,7 @@ public class PlayerNotificationManager {
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification();
@@ -967,7 +971,7 @@ public class PlayerNotificationManager {
@Override
public void onRepeatModeChanged(int repeatMode) {
- if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification();
@@ -985,7 +989,8 @@ public class PlayerNotificationManager {
@Override
public void onReceive(Context context, Intent intent) {
- if (!isNotificationStarted) {
+ Player player = PlayerNotificationManager.this.player;
+ if (player == null || !isNotificationStarted) {
return;
}
String action = intent.getAction();
@@ -1013,7 +1018,7 @@ public class PlayerNotificationManager {
} else if (ACTION_STOP.equals(action)) {
controlDispatcher.dispatchStop(player, true);
stopNotification();
- } else if (customActions.containsKey(action)) {
+ } else if (customActionReceiver != null && customActions.containsKey(action)) {
customActionReceiver.onCustomAction(player, action, intent);
}
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
index 25c4318768..a7aa48c0db 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
@@ -133,6 +133,12 @@ import java.util.List;
* Corresponding method: {@link #setShutterBackgroundColor(int)}
* Default: {@code unset}
*
+ * {@code keep_content_on_player_reset} - Whether the currently displayed video frame
+ * or media artwork is kept visible when the player is reset.
+ *
+ * - Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
+ *
- Default: {@code false}
+ *
* {@code player_layout_id} - Specifies the id of the layout to be inflated. See below
* for more details.
*
@@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout {
private boolean useArtwork;
private Bitmap defaultArtwork;
private boolean showBuffering;
+ private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage;
private int controllerShowTimeoutMs;
@@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout {
a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering);
+ keepContentOnPlayerReset =
+ a.getBoolean(
+ R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
controllerHideDuringAds =
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
} finally {
@@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout {
if (useController) {
controller.setPlayer(player);
}
- if (shutterView != null) {
- shutterView.setVisibility(VISIBLE);
- }
if (subtitleView != null) {
subtitleView.setCues(null);
}
updateBuffering();
updateErrorMessage();
+ updateForCurrentTrackSelections(/* isNewPlayer= */ true);
if (player != null) {
Player.VideoComponent newVideoComponent = player.getVideoComponent();
if (newVideoComponent != null) {
@@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout {
}
player.addListener(componentListener);
maybeShowController(false);
- updateForCurrentTrackSelections();
} else {
hideController();
- hideArtwork();
}
}
@@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout {
Assertions.checkState(!useArtwork || artworkView != null);
if (this.useArtwork != useArtwork) {
this.useArtwork = useArtwork;
- updateForCurrentTrackSelections();
+ updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
}
@@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout {
public void setDefaultArtwork(Bitmap defaultArtwork) {
if (this.defaultArtwork != defaultArtwork) {
this.defaultArtwork = defaultArtwork;
- updateForCurrentTrackSelections();
+ updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
}
@@ -600,6 +606,32 @@ public class PlayerView extends FrameLayout {
}
}
+ /**
+ * Sets whether the currently displayed video frame or media artwork is kept visible when the
+ * player is reset. A player reset is defined to mean the player being re-prepared with different
+ * media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being
+ * replaced or cleared by calling {@link #setPlayer(Player)}.
+ *
+ * If enabled, the currently displayed video frame or media artwork will be kept visible until
+ * the player set on the view has been successfully prepared with new media and loaded enough of
+ * it to have determined the available tracks. Hence enabling this option allows transitioning
+ * from playing one piece of media to another, or from using one player instance to another,
+ * without clearing the view's content.
+ *
+ *
If disabled, the currently displayed video frame or media artwork will be hidden as soon as
+ * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible.
+ * Hence the video frame will not be hidden if using a custom layout that omits this view.
+ *
+ * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is
+ * kept visible when the player is reset.
+ */
+ public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
+ if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) {
+ this.keepContentOnPlayerReset = keepContentOnPlayerReset;
+ updateForCurrentTrackSelections(/* isNewPlayer= */ false);
+ }
+ }
+
/**
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default.
@@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout {
return player != null && player.isPlayingAd() && player.getPlayWhenReady();
}
- private void updateForCurrentTrackSelections() {
- if (player == null) {
+ private void updateForCurrentTrackSelections(boolean isNewPlayer) {
+ if (player == null || player.getCurrentTrackGroups().isEmpty()) {
+ if (!keepContentOnPlayerReset) {
+ hideArtwork();
+ closeShutter();
+ }
return;
}
+
+ if (isNewPlayer && !keepContentOnPlayerReset) {
+ // Hide any video from the previous player.
+ closeShutter();
+ }
+
TrackSelectionArray selections = player.getCurrentTrackSelections();
for (int i = 0; i < selections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
@@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout {
return;
}
}
+
// Video disabled so the shutter must be closed.
- if (shutterView != null) {
- shutterView.setVisibility(VISIBLE);
- }
+ closeShutter();
// Display artwork if enabled and available, else hide it.
if (useArtwork) {
for (int i = 0; i < selections.length; i++) {
@@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout {
}
}
+ private void closeShutter() {
+ if (shutterView != null) {
+ shutterView.setVisibility(View.VISIBLE);
+ }
+ }
+
private void updateBuffering() {
if (bufferingView != null) {
boolean showBufferingSpinner =
@@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout {
@Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
- updateForCurrentTrackSelections();
+ updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
// Player.EventListener implementation
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java
index b6cfc9a6f3..c5d264b310 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java
@@ -372,12 +372,22 @@ import com.google.android.exoplayer2.util.Util;
float previousBottom = layout.getLineTop(0);
int lineCount = layout.getLineCount();
for (int i = 0; i < lineCount; i++) {
- lineBounds.left = layout.getLineLeft(i) - textPaddingX;
- lineBounds.right = layout.getLineRight(i) + textPaddingX;
+ float lineTextBoundLeft = layout.getLineLeft(i);
+ float lineTextBoundRight = layout.getLineRight(i);
+ lineBounds.left = lineTextBoundLeft - textPaddingX;
+ lineBounds.right = lineTextBoundRight + textPaddingX;
lineBounds.top = previousBottom;
lineBounds.bottom = layout.getLineBottom(i);
previousBottom = lineBounds.bottom;
- canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
+ float lineTextWidth = lineTextBoundRight - lineTextBoundLeft;
+ if (lineTextWidth > 0) {
+ // Do not draw a line's background color if it has no text.
+ // For some reason, calculating the width manually is more reliable than
+ // layout.getLineWidth().
+ // Sometimes, lineTextBoundRight == lineTextBoundLeft, and layout.getLineWidth() still
+ // returns non-zero value.
+ canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
+ }
}
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
index d89f82b7c4..4dbd4d5fec 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
@@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput {
*/
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
- private static final int FRACTIONAL = 0;
- private static final int FRACTIONAL_IGNORE_PADDING = 1;
- private static final int ABSOLUTE = 2;
-
private final List painters;
private List cues;
- private int textSizeType;
+ private @Cue.TextSizeType int textSizeType;
private float textSize;
private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes;
@@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput {
public SubtitleView(Context context, AttributeSet attrs) {
super(context, attrs);
painters = new ArrayList<>();
- textSizeType = FRACTIONAL;
+ textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true;
@@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput {
} else {
resources = context.getResources();
}
- setTextSize(ABSOLUTE, TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
+ setTextSize(
+ Cue.TEXT_SIZE_TYPE_ABSOLUTE,
+ TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
}
/**
@@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput {
* height after the top and bottom padding has been subtracted.
*/
public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) {
- setTextSize(ignorePadding ? FRACTIONAL_IGNORE_PADDING : FRACTIONAL, fractionOfHeight);
+ setTextSize(
+ ignorePadding
+ ? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING
+ : Cue.TEXT_SIZE_TYPE_FRACTIONAL,
+ fractionOfHeight);
}
- private void setTextSize(int textSizeType, float textSize) {
+ private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) {
return;
}
@@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput {
// No space to draw subtitles.
return;
}
+ int rawViewHeight = rawBottom - rawTop;
+ int viewHeightMinusPadding = bottom - top;
- float textSizePx = textSizeType == ABSOLUTE ? textSize
- : textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop));
- if (textSizePx <= 0) {
+ float defaultViewTextSizePx =
+ resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
+ if (defaultViewTextSizePx <= 0) {
// Text has no height.
return;
}
for (int i = 0; i < cueCount; i++) {
- painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style,
- textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom);
+ Cue cue = cues.get(i);
+ float textSizePx =
+ resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
+ SubtitlePainter painter = painters.get(i);
+ painter.draw(
+ cue,
+ applyEmbeddedStyles,
+ applyEmbeddedFontSizes,
+ style,
+ textSizePx,
+ bottomPaddingFraction,
+ canvas,
+ left,
+ top,
+ right,
+ bottom);
+ }
+ }
+
+ private float resolveTextSizeForCue(
+ Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
+ if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
+ return defaultViewTextSizePx;
+ }
+ float defaultCueTextSizePx =
+ resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
+ return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx;
+ }
+
+ private float resolveTextSize(
+ @Cue.TextSizeType int textSizeType,
+ float textSize,
+ int rawViewHeight,
+ int viewHeightMinusPadding) {
+ switch (textSizeType) {
+ case Cue.TEXT_SIZE_TYPE_ABSOLUTE:
+ return textSize;
+ case Cue.TEXT_SIZE_TYPE_FRACTIONAL:
+ return textSize * viewHeightMinusPadding;
+ case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:
+ return textSize * rawViewHeight;
+ case Cue.TYPE_UNSET:
+ default:
+ return Cue.DIMEN_UNSET;
}
}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
index 45ccd783e7..be0babf5a8 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java
@@ -21,6 +21,8 @@ import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
+import android.support.annotation.AttrRes;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
@@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout {
private int rendererIndex;
private TrackGroupArray trackGroups;
private boolean isDisabled;
- private SelectionOverride override;
+ private @Nullable SelectionOverride override;
/**
* Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
@@ -100,11 +102,13 @@ public class TrackSelectionView extends LinearLayout {
this(context, null);
}
- public TrackSelectionView(Context context, AttributeSet attrs) {
+ public TrackSelectionView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
- public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr) {
+ @SuppressWarnings("nullness")
+ public TrackSelectionView(
+ Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray attributeArray =
context
@@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout {
* @param allowAdaptiveSelections Whether adaptive selection is enabled.
*/
public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {
- if (!this.allowAdaptiveSelections == allowAdaptiveSelections) {
+ if (this.allowAdaptiveSelections != allowAdaptiveSelections) {
this.allowAdaptiveSelections = allowAdaptiveSelections;
updateViews();
}
@@ -168,12 +172,14 @@ public class TrackSelectionView extends LinearLayout {
}
/**
- * Sets the {@link TrackNameProvider} used to generate the user visible name of each track.
+ * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and
+ * updates the view with track names queried from the specified provider.
*
* @param trackNameProvider The {@link TrackNameProvider} to use.
*/
public void setTrackNameProvider(TrackNameProvider trackNameProvider) {
this.trackNameProvider = Assertions.checkNotNull(trackNameProvider);
+ updateViews();
}
/**
@@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout {
override = new SelectionOverride(groupIndex, trackIndex);
} else {
// An existing override is being modified.
- boolean isEnabled = ((CheckedTextView) view).isChecked();
int overrideLength = override.length;
- if (isEnabled) {
+ int[] overrideTracks = override.tracks;
+ if (((CheckedTextView) view).isChecked()) {
// Remove the track from the override.
if (overrideLength == 1) {
// The last track is being removed, so the override becomes empty.
override = null;
isDisabled = true;
} else {
- int[] tracks = getTracksRemoving(override.tracks, trackIndex);
+ int[] tracks = getTracksRemoving(overrideTracks, trackIndex);
override = new SelectionOverride(groupIndex, tracks);
}
} else {
- int[] tracks = getTracksAdding(override.tracks, trackIndex);
+ int[] tracks = getTracksAdding(overrideTracks, trackIndex);
override = new SelectionOverride(groupIndex, tracks);
}
}
diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml
index 3e83396678..6ac92acf9d 100644
--- a/library/ui/src/main/res/values-de/strings.xml
+++ b/library/ui/src/main/res/values-de/strings.xml
@@ -21,7 +21,7 @@
Video
Audio
Text
- Keiner
+ Ohne
Automatisch
Unbekannt
%1$d × %2$d
@@ -31,5 +31,5 @@
5.1-Surround-Sound
7.1-Surround-Sound
%1$.2f Mbit/s
- %1$s und %2$s
+ %1$s, %2$s
diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml
index 9eefc027ed..e127f181e9 100644
--- a/library/ui/src/main/res/values/attrs.xml
+++ b/library/ui/src/main/res/values/attrs.xml
@@ -51,6 +51,7 @@
+
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java
index 2675e1f0d7..de623b59c9 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java
@@ -217,6 +217,11 @@ public class FakeDataSource implements DataSource {
return dataSpecs;
}
+ /** Returns whether the data source is currently opened. */
+ public final boolean isOpened() {
+ return opened;
+ }
+
protected void onDataRead(int bytesRead) throws IOException {
// Do nothing. Can be overridden.
}
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java
index 905adae092..ffc877bf42 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java
@@ -159,6 +159,11 @@ public class FakeMediaSource extends BaseMediaSource {
}
}
+ /** Asserts that the source has been prepared. */
+ public void assertPrepared() {
+ assertThat(preparedSource).isTrue();
+ }
+
/**
* Assert that the source and all periods have been released.
*/
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java
index 639cb82c2d..6432842df4 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java
@@ -26,6 +26,8 @@ import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
/**
* A fake {@link TrackOutput}.
@@ -114,6 +116,26 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable {
sampleEndOffsets.get(index));
}
+ public long getSampleTimeUs(int index) {
+ return sampleTimesUs.get(index);
+ }
+
+ public int getSampleFlags(int index) {
+ return sampleFlags.get(index);
+ }
+
+ public CryptoData getSampleCryptoData(int index) {
+ return cryptoDatas.get(index);
+ }
+
+ public int getSampleCount() {
+ return sampleTimesUs.size();
+ }
+
+ public List getSampleTimesUs() {
+ return Collections.unmodifiableList(sampleTimesUs);
+ }
+
public void assertEquals(FakeTrackOutput expected) {
assertThat(format).isEqualTo(expected.format);
assertThat(sampleTimesUs).hasSize(expected.sampleTimesUs.size());