Commit 2b55c91a by Andrew Lewis Committed by GitHub

Merge pull request #4281 from google/dev-2.8.1-rc

r2.8.1
parents 000f3f23 7e38b269
Showing with 1600 additions and 569 deletions
# Release notes # # Release notes #
### 2.8.1 ###
* HLS:
* Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
([#4239](https://github.com/google/ExoPlayer/issues/4239)).
* Fix playback of clipped streams starting from non-keyframe positions
([#4241](https://github.com/google/ExoPlayer/issues/4241)).
* OkHttp extension: Fix to correctly include response headers in thrown
`InvalidResponseCodeException`s.
* Add possibility to cancel `PlayerMessage`s.
* UI components:
* Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed
video frame or media artwork visible when the player is reset
([#2843](https://github.com/google/ExoPlayer/issues/2843)).
* Fix crash when switching surface on Moto E(4)
([#4134](https://github.com/google/ExoPlayer/issues/4134)).
* Fix a bug that could cause event listeners to be called with inconsistent
information if an event listener interacted with the player
([#4262](https://github.com/google/ExoPlayer/issues/4262)).
* Audio:
* Fix extraction of PCM in MP4/MOV
([#4228](https://github.com/google/ExoPlayer/issues/4228)).
* FLAC: Supports seeking for FLAC files without SEEKTABLE
([#1808](https://github.com/google/ExoPlayer/issues/1808)).
* Captions:
* TTML:
* Fix a styling issue when there are multiple regions displayed at the same
time that can make text size of each region much smaller than defined.
* Fix an issue when the caption line has no text (empty line or only line
break), and the line's background is still displayed.
* Support TTML font size using % correctly (as percentage of document cell
resolution).
### 2.8.0 ### ### 2.8.0 ###
* Downloading: * Downloading:
...@@ -75,7 +108,7 @@ ...@@ -75,7 +108,7 @@
* Allow multiple listeners for `DefaultDrmSessionManager`. * Allow multiple listeners for `DefaultDrmSessionManager`.
* Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.
* Change minimum API requirement for CBC and pattern encryption from 24 to 25 * Change minimum API requirement for CBC and pattern encryption from 24 to 25
([#4022][https://github.com/google/ExoPlayer/issues/4022]). ([#4022](https://github.com/google/ExoPlayer/issues/4022)).
* Fix handling of 307/308 redirects when making license requests * Fix handling of 307/308 redirects when making license requests
([#4108](https://github.com/google/ExoPlayer/issues/4108)). ([#4108](https://github.com/google/ExoPlayer/issues/4108)).
* HLS: * HLS:
......
<!-- 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.
-->
<lint>
<issue id="InvalidPackage">
<ignore path="**/checker-qual-*.jar"/>
</issue>
</lint>
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.8.0' releaseVersion = '2.8.1'
releaseVersionCode = 2800 releaseVersionCode = 2801
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
...@@ -33,6 +33,7 @@ project.ext { ...@@ -33,6 +33,7 @@ project.ext {
robolectricVersion = '3.7.1' robolectricVersion = '3.7.1'
autoValueVersion = '1.6' autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.0.2'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
package="com.google.android.exoplayer2.demo"> package="com.google.android.exoplayer2.demo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.Nullable;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
...@@ -110,7 +111,7 @@ import java.util.Map; ...@@ -110,7 +111,7 @@ import java.util.Map;
// equals and hashCode implementations. // equals and hashCode implementations.
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (this == other) { if (this == other) {
return true; return true;
} else if (!(other instanceof CastTimeline)) { } else if (!(other instanceof CastTimeline)) {
......
...@@ -70,7 +70,8 @@ COMMON_OPTIONS="\ ...@@ -70,7 +70,8 @@ COMMON_OPTIONS="\
--enable-decoder=flac \ --enable-decoder=flac \
" && \ " && \
cd "${FFMPEG_EXT_PATH}/jni" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && \
./configure \ ./configure \
--libdir=android-libs/armeabi-v7a \ --libdir=android-libs/armeabi-v7a \
--arch=arm \ --arch=arm \
......
/*
* 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 static com.google.common.truth.Truth.assertThat;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
/** Unit test for {@link FlacBinarySearchSeeker}. */
public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
@Override
protected void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException {
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.hasPendingSeek()).isTrue();
}
}
...@@ -92,18 +92,14 @@ import java.util.List; ...@@ -92,18 +92,14 @@ import java.util.List;
} }
decoderJni.setData(inputBuffer.data); decoderJni.setData(inputBuffer.data);
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
int result;
try { try {
result = decoderJni.decodeSample(outputData); decoderJni.decodeSample(outputData);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
return new FlacDecoderException("Frame decoding failed", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (result < 0) {
return new FlacDecoderException("Frame decoding failed");
}
outputData.position(0);
outputData.limit(result);
return null; return null;
} }
......
...@@ -26,6 +26,17 @@ import java.nio.ByteBuffer; ...@@ -26,6 +26,17 @@ import java.nio.ByteBuffer;
*/ */
/* package */ final class FlacDecoderJni { /* 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 static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private final long nativeDecoderContext; private final long nativeDecoderContext;
...@@ -116,14 +127,50 @@ import java.nio.ByteBuffer; ...@@ -116,14 +127,50 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); return flacDecodeMetadata(nativeDecoderContext);
} }
public int decodeSample(ByteBuffer output) throws IOException, InterruptedException { /**
return output.isDirect() * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
? flacDecodeToBuffer(nativeDecoderContext, output) * error occurs, resets the stream and input to the given {@code retryPosition}.
: flacDecodeToArray(nativeDecoderContext, output.array()); *
* @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; ...@@ -133,8 +180,19 @@ import java.nio.ByteBuffer;
return flacGetDecodePosition(nativeDecoderContext); return flacGetDecodePosition(nativeDecoderContext);
} }
public long getLastSampleTimestamp() { /** Returns the timestamp for the first sample in the last decoded frame. */
return flacGetLastTimestamp(nativeDecoderContext); 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; ...@@ -153,6 +211,11 @@ import java.nio.ByteBuffer;
return flacGetStateString(nativeDecoderContext); return flacGetStateString(nativeDecoderContext);
} }
/** Returns whether the decoder has read to the end of the input. */
public boolean isDecoderAtEndOfInput() {
return flacIsDecoderAtEndOfStream(nativeDecoderContext);
}
public void flush() { public void flush() {
flacFlush(nativeDecoderContext); flacFlush(nativeDecoderContext);
} }
...@@ -181,18 +244,34 @@ import java.nio.ByteBuffer; ...@@ -181,18 +244,34 @@ import java.nio.ByteBuffer;
} }
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamInfo flacDecodeMetadata(long context)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToArray(long context, byte[] outputArray) private native int flacDecodeToArray(long context, byte[] outputArray)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native long flacGetDecodePosition(long context); 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 long flacGetSeekPosition(long context, long timeUs);
private native String flacGetStateString(long context); private native String flacGetStateString(long context);
private native boolean flacIsDecoderAtEndOfStream(long context);
private native void flacFlush(long context); private native void flacFlush(long context);
private native void flacReset(long context, long newPosition); private native void flacReset(long context, long newPosition);
private native void flacRelease(long context); private native void flacRelease(long context);
} }
...@@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor { ...@@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor {
private ParsableByteArray outputBuffer; private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer; private ByteBuffer outputByteBuffer;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata; private Metadata id3Metadata;
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
private boolean metadataParsed; private boolean readPastStreamInfo;
/** Constructs an instance with flags = 0. */ /** Constructs an instance with flags = 0. */
public FlacExtractor() { public FlacExtractor() {
...@@ -136,83 +138,43 @@ public final class FlacExtractor implements Extractor { ...@@ -136,83 +138,43 @@ public final class FlacExtractor implements Extractor {
} }
decoderJni.setData(input); decoderJni.setData(input);
readPastStreamInfo(input);
if (!metadataParsed) { if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
final FlacStreamInfo streamInfo; return handlePendingSeek(input, seekPosition);
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);
} }
outputBuffer.reset();
long lastDecodePosition = decoderJni.getDecodePosition(); long lastDecodePosition = decoderJni.getDecodePosition();
int size;
try { try {
size = decoderJni.decodeSample(outputByteBuffer); decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (IOException e) { } catch (FlacDecoderJni.FlacFrameDecodeException e) {
if (lastDecodePosition >= 0) { throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
decoderJni.reset(lastDecodePosition);
input.setRetryPosition(lastDecodePosition, e);
}
throw e;
} }
if (size <= 0) { int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT; 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; return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
} }
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
metadataParsed = false; readPastStreamInfo = false;
} }
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.reset(position); decoderJni.reset(position);
} }
if (flacBinarySearchSeeker != null) {
flacBinarySearchSeeker.setSeekTargetUs(timeUs);
}
} }
@Override @Override
public void release() { public void release() {
flacBinarySearchSeeker = null;
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.release(); decoderJni.release();
decoderJni = null; decoderJni = null;
...@@ -244,6 +206,100 @@ public final class FlacExtractor implements Extractor { ...@@ -244,6 +206,100 @@ public final class FlacExtractor implements Extractor {
return Arrays.equals(header, FLAC_SIGNATURE); 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 static final class FlacSeekMap implements SeekMap {
private final long durationUs; private final long durationUs;
......
...@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) { ...@@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {
return context->parser->getDecodePosition(); return context->parser->getDecodePosition();
} }
DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) { DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastTimestamp(); return context->parser->getLastFrameTimestamp();
}
DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastFrameFirstSampleIndex();
}
DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getNextFrameFirstSampleIndex();
} }
DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
...@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { ...@@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
return env->NewStringUTF(str); return env->NewStringUTF(str);
} }
DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->isDecoderAtEndOfStream();
}
DECODER_FUNC(void, flacFlush, jlong jContext) { DECODER_FUNC(void, flacFlush, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
context->parser->flush(); context->parser->flush();
......
...@@ -44,10 +44,18 @@ class FLACParser { ...@@ -44,10 +44,18 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
int64_t getLastTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); 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(); bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size); size_t readBuffer(void *output, size_t output_size);
...@@ -83,6 +91,11 @@ class FLACParser { ...@@ -83,6 +91,11 @@ class FLACParser {
return FLAC__stream_decoder_get_resolved_state_string(mDecoder); 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: private:
DataSource *mDataSource; DataSource *mDataSource;
......
...@@ -649,18 +649,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -649,18 +649,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override @Override
public void loadAd(String adUriString) { 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 { 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); int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) { if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
......
...@@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource {
// Check for a valid response code. // Check for a valid response code.
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
Map<String, List<String>> headers = request.headers().toMultimap(); Map<String, List<String>> headers = response.headers().toMultimap();
closeConnectionQuietly(); closeConnectionQuietly();
InvalidResponseCodeException exception = new InvalidResponseCodeException( InvalidResponseCodeException exception = new InvalidResponseCodeException(
responseCode, headers, dataSpec); responseCode, headers, dataSpec);
......
...@@ -22,6 +22,13 @@ android { ...@@ -22,6 +22,13 @@ android {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' 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. // Workaround to prevent circular dependency on project :testutils.
...@@ -42,19 +49,17 @@ android { ...@@ -42,19 +49,17 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion 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:' + dexmakerVersion
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion 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 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import static com.google.common.truth.Truth.assertThat; 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.ContentProvider;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
...@@ -28,48 +28,58 @@ import android.os.Bundle; ...@@ -28,48 +28,58 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; 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.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
/** /** Unit tests for {@link ContentDataSource}. */
* Unit tests for {@link ContentDataSource}. @RunWith(AndroidJUnit4.class)
*/ public final class ContentDataSourceTest {
public final class ContentDataSourceTest extends InstrumentationTestCase {
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
@Test
public void testRead() throws Exception { public void testRead() throws Exception {
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); assertData(0, C.LENGTH_UNSET, false);
} }
@Test
public void testReadPipeMode() throws Exception { public void testReadPipeMode() throws Exception {
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); assertData(0, C.LENGTH_UNSET, true);
} }
@Test
public void testReadFixedLength() throws Exception { public void testReadFixedLength() throws Exception {
assertData(getInstrumentation(), 0, 100, false); assertData(0, 100, false);
} }
@Test
public void testReadFromOffsetToEndOfInput() throws Exception { public void testReadFromOffsetToEndOfInput() throws Exception {
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); assertData(1, C.LENGTH_UNSET, false);
} }
@Test
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); assertData(1, C.LENGTH_UNSET, true);
} }
@Test
public void testReadFromOffsetFixedLength() throws Exception { public void testReadFromOffsetFixedLength() throws Exception {
assertData(getInstrumentation(), 1, 100, false); assertData(1, 100, false);
} }
@Test
public void testReadInvalidUri() throws Exception { 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); Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
DataSpec dataSpec = new DataSpec(contentUri); DataSpec dataSpec = new DataSpec(contentUri);
try { try {
...@@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ...@@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
} }
} }
private static void assertData(Instrumentation instrumentation, int offset, int length, private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
boolean pipeMode) throws IOException {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); ContentDataSource dataSource =
new ContentDataSource(InstrumentationRegistry.getTargetContext());
try { try {
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); 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, byte[] expectedData = Arrays.copyOfRange(completeData, offset,
length == C.LENGTH_UNSET ? completeData.length : offset + length); length == C.LENGTH_UNSET ? completeData.length : offset + length);
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
......
...@@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -29,9 +30,14 @@ import java.io.FileOutputStream; ...@@ -29,9 +30,14 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests {@link CachedContentIndex}. */ /** Tests {@link CachedContentIndex}. */
public class CachedContentIndexTest extends InstrumentationTestCase { @RunWith(AndroidJUnit4.class)
public class CachedContentIndexTest {
private final byte[] testIndexV1File = { private final byte[] testIndexV1File = {
0, 0, 0, 1, // version 0, 0, 0, 1, // version
...@@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
private CachedContentIndex index; private CachedContentIndex index;
private File cacheDir; private File cacheDir;
@Override @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); cacheDir =
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @After
protected void tearDown() throws Exception { public void tearDown() {
Util.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
super.tearDown();
} }
@Test
public void testAddGetRemove() throws Exception { public void testAddGetRemove() throws Exception {
final String key1 = "key1"; final String key1 = "key1";
final String key2 = "key2"; final String key2 = "key2";
...@@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(cacheSpanFile.exists()).isTrue(); assertThat(cacheSpanFile.exists()).isTrue();
} }
@Test
public void testStoreAndLoad() throws Exception { public void testStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
} }
@Test
public void testLoadV1() throws Exception { public void testLoadV1() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV1File); fos.write(testIndexV1File);
...@@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
} }
@Test
public void testLoadV2() throws Exception { public void testLoadV2() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV2File); fos.write(testIndexV2File);
...@@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
} }
public void testAssignIdForKeyAndGetKeyForId() throws Exception { @Test
public void testAssignIdForKeyAndGetKeyForId() {
final String key1 = "key1"; final String key1 = "key1";
final String key2 = "key2"; final String key2 = "key2";
int id1 = index.assignIdForKey(key1); int id1 = index.assignIdForKey(key1);
...@@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.assignIdForKey(key2)).isEqualTo(id2); assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
} }
public void testGetNewId() throws Exception { @Test
public void testGetNewId() {
SparseArray<String> idToKey = new SparseArray<>(); SparseArray<String> idToKey = new SparseArray<>();
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
idToKey.put(10, ""); idToKey.put(10, "");
...@@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
} }
@Test
public void testEncryption() throws Exception { public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".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 { ...@@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
} }
public void testRemoveEmptyNotLockedCachedContent() throws Exception { @Test
public void testRemoveEmptyNotLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
index.maybeRemove(cachedContent.key); index.maybeRemove(cachedContent.key);
...@@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNull(); assertThat(index.get(cachedContent.key)).isNull();
} }
@Test
public void testCantRemoveNotEmptyCachedContent() throws Exception { public void testCantRemoveNotEmptyCachedContent() throws Exception {
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
File cacheSpanFile = File cacheSpanFile =
...@@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNotNull(); assertThat(index.get(cachedContent.key)).isNotNull();
} }
public void testCantRemoveLockedCachedContent() throws Exception { @Test
public void testCantRemoveLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true); cachedContent.setLocked(true);
......
...@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -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.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; 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 com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
...@@ -26,11 +27,14 @@ import java.io.IOException; ...@@ -26,11 +27,14 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; 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}. */
* Unit tests for {@link SimpleCacheSpan}. @RunWith(AndroidJUnit4.class)
*/ public class SimpleCacheSpanTest {
public class SimpleCacheSpanTest extends InstrumentationTestCase {
private CachedContentIndex index; private CachedContentIndex index;
private File cacheDir; private File cacheDir;
...@@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { ...@@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
return SimpleCacheSpan.createCacheEntry(cacheFile, index); return SimpleCacheSpan.createCacheEntry(cacheFile, index);
} }
@Override @Before
protected void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); cacheDir =
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @After
protected void tearDown() throws Exception { public void tearDown() {
Util.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
super.tearDown();
} }
@Test
public void testCacheFile() throws Exception { public void testCacheFile() throws Exception {
assertCacheSpan("key1", 0, 0); assertCacheSpan("key1", 0, 0);
assertCacheSpan("key2", 1, 2); assertCacheSpan("key2", 1, 2);
...@@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { ...@@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
+ "A paragraph-separator character \u2029", 1, 2); + "A paragraph-separator character \u2029", 1, 2);
} }
@Test
public void testUpgradeFileName() throws Exception { public void testUpgradeFileName() throws Exception {
String key = "asd\u00aa"; String key = "asd\u00aa";
int id = index.assignIdForKey(key); int id = index.assignIdForKey(key);
......
...@@ -46,11 +46,10 @@ public class DefaultLoadControl implements LoadControl { ...@@ -46,11 +46,10 @@ public class DefaultLoadControl implements LoadControl {
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; 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, * The default duration of media that must be buffered for playback to resume after a rebuffer, in
* in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.
* 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 * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
......
...@@ -185,10 +185,6 @@ public interface ExoPlayer extends Player { ...@@ -185,10 +185,6 @@ public interface ExoPlayer extends Player {
*/ */
Looper getPlaybackLooper(); Looper getPlaybackLooper();
@Override
@Nullable
ExoPlaybackException getPlaybackError();
/** /**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to * Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}. * {@code prepare(mediaSource, true, true)}.
......
...@@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.playWhenReady != playWhenReady) { if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
internalPlayer.setPlayWhenReady(playWhenReady); internalPlayer.setPlayWhenReady(playWhenReady);
PlaybackInfo playbackInfo = this.playbackInfo;
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
} }
...@@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
break; break;
case ExoPlayerImplInternal.MSG_ERROR: case ExoPlayerImplInternal.MSG_ERROR:
playbackError = (ExoPlaybackException) msg.obj; ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
this.playbackError = playbackError;
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
listener.onPlayerError(playbackError); listener.onPlayerError(playbackError);
} }
...@@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
boolean trackSelectorResultChanged = boolean trackSelectorResultChanged =
this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
playbackInfo = newPlaybackInfo; playbackInfo = newPlaybackInfo;
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
......
...@@ -854,6 +854,9 @@ import java.util.Collections; ...@@ -854,6 +854,9 @@ import java.util.Collections;
} }
private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
if (message.isCanceled()) {
return;
}
try { try {
message.getTarget().handleMessage(message.getType(), message.getPayload()); message.getTarget().handleMessage(message.getType(), message.getPayload());
} finally { } finally {
...@@ -945,7 +948,7 @@ import java.util.Collections; ...@@ -945,7 +948,7 @@ import java.util.Collections;
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
sendMessageToTarget(nextInfo.message); sendMessageToTarget(nextInfo.message);
if (nextInfo.message.getDeleteAfterDelivery()) { if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
pendingMessages.remove(nextPendingMessageIndex); pendingMessages.remove(nextPendingMessageIndex);
} else { } else {
nextPendingMessageIndex++; nextPendingMessageIndex++;
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** 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. // 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}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
...@@ -87,7 +88,7 @@ public final class PlaybackParameters { ...@@ -87,7 +88,7 @@ public final class PlaybackParameters {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -63,6 +63,7 @@ public final class PlayerMessage { ...@@ -63,6 +63,7 @@ public final class PlayerMessage {
private boolean isSent; private boolean isSent;
private boolean isDelivered; private boolean isDelivered;
private boolean isProcessed; private boolean isProcessed;
private boolean isCanceled;
/** /**
* Creates a new message. * Creates a new message.
...@@ -243,6 +244,24 @@ public final class PlayerMessage { ...@@ -243,6 +244,24 @@ public final class PlayerMessage {
} }
/** /**
* 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 * Blocks until after the message has been delivered or the player is no longer able to deliver
* the message. * the message.
* *
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
/** /**
* The configuration of a {@link Renderer}. * The configuration of a {@link Renderer}.
*/ */
...@@ -41,7 +43,7 @@ public final class RendererConfiguration { ...@@ -41,7 +43,7 @@ public final class RendererConfiguration {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
...@@ -71,7 +72,7 @@ public final class SeekParameters { ...@@ -71,7 +72,7 @@ public final class SeekParameters {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player ...@@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
private float audioVolume; private float audioVolume;
private MediaSource mediaSource; private MediaSource mediaSource;
private List<Cue> currentCues;
/** /**
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @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 ...@@ -177,6 +178,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioAttributes = AudioAttributes.DEFAULT; audioAttributes = AudioAttributes.DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
currentCues = Collections.emptyList();
// Build the player and associated objects. // Build the player and associated objects.
player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
...@@ -502,6 +504,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player ...@@ -502,6 +504,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override @Override
public void addTextOutput(TextOutput listener) { public void addTextOutput(TextOutput listener) {
if (!currentCues.isEmpty()) {
listener.onCues(currentCues);
}
textOutputs.add(listener); textOutputs.add(listener);
} }
...@@ -775,6 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player ...@@ -775,6 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
mediaSource = null; mediaSource = null;
analyticsCollector.resetForNewMediaSource(); analyticsCollector.resetForNewMediaSource();
} }
currentCues = Collections.emptyList();
} }
@Override @Override
...@@ -790,6 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player ...@@ -790,6 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
if (mediaSource != null) { if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector); mediaSource.removeEventListener(analyticsCollector);
} }
currentCues = Collections.emptyList();
} }
@Override @Override
...@@ -1095,6 +1102,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player ...@@ -1095,6 +1102,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override @Override
public void onCues(List<Cue> cues) { public void onCues(List<Cue> cues) {
currentCues = cues;
for (TextOutput textOutput : textOutputs) { for (TextOutput textOutput : textOutputs) {
textOutput.onCues(cues); textOutput.onCues(cues);
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
/** /**
...@@ -119,7 +120,7 @@ public final class AudioAttributes { ...@@ -119,7 +120,7 @@ public final class AudioAttributes {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -22,6 +22,7 @@ import android.content.Intent; ...@@ -22,6 +22,7 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
/** /**
...@@ -96,7 +97,7 @@ public final class AudioCapabilities { ...@@ -96,7 +97,7 @@ public final class AudioCapabilities {
} }
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (this == other) { if (this == other) {
return true; return true;
} }
......
...@@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
...@@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SchemeData)) { if (!(obj instanceof SchemeData)) {
return false; return false;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -92,7 +93,7 @@ public interface SeekMap { ...@@ -92,7 +93,7 @@ public interface SeekMap {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import android.support.annotation.Nullable;
/** Defines a seek point in a media stream. */ /** Defines a seek point in a media stream. */
public final class SeekPoint { public final class SeekPoint {
...@@ -42,7 +44,7 @@ public final class SeekPoint { ...@@ -42,7 +44,7 @@ public final class SeekPoint {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -69,7 +70,7 @@ public interface TrackOutput { ...@@ -69,7 +70,7 @@ public interface TrackOutput {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -189,11 +189,13 @@ import java.util.List; ...@@ -189,11 +189,13 @@ import java.util.List;
} }
} }
// True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. // Fixed sample size raw audio may need to be rechunked.
boolean isRechunkable = sampleSizeBox.isFixedSampleSize() boolean isFixedSampleSizeRawAudio =
&& MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) sampleSizeBox.isFixedSampleSize()
&& remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
&& remainingSynchronizationSamples == 0; && remainingTimestampDeltaChanges == 0
&& remainingTimestampOffsetChanges == 0
&& remainingSynchronizationSamples == 0;
long[] offsets; long[] offsets;
int[] sizes; int[] sizes;
...@@ -203,7 +205,7 @@ import java.util.List; ...@@ -203,7 +205,7 @@ import java.util.List;
long timestampTimeUnits = 0; long timestampTimeUnits = 0;
long duration; long duration;
if (!isRechunkable) { if (!isFixedSampleSizeRawAudio) {
offsets = new long[sampleCount]; offsets = new long[sampleCount];
sizes = new int[sampleCount]; sizes = new int[sampleCount];
timestamps = new long[sampleCount]; timestamps = new long[sampleCount];
...@@ -296,7 +298,8 @@ import java.util.List; ...@@ -296,7 +298,8 @@ import java.util.List;
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
} }
int fixedSampleSize = sampleSizeBox.readNextSampleSize(); int fixedSampleSize =
Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
offsets = rechunkedResults.offsets; offsets = rechunkedResults.offsets;
...@@ -1224,7 +1227,7 @@ import java.util.List; ...@@ -1224,7 +1227,7 @@ import java.util.List;
stsc.setPosition(Atom.FULL_HEADER_SIZE); stsc.setPosition(Atom.FULL_HEADER_SIZE);
remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
index = C.INDEX_UNSET; index = -1;
} }
public boolean moveNext() { public boolean moveNext() {
......
...@@ -482,13 +482,13 @@ public final class MediaCodecUtil { ...@@ -482,13 +482,13 @@ public final class MediaCodecUtil {
return null; return null;
} }
Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger); int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);
if (profile == null) { if (profile == -1) {
Log.w(TAG, "Unknown AVC profile: " + profileInteger); Log.w(TAG, "Unknown AVC profile: " + profileInteger);
return null; return null;
} }
Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger); int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);
if (level == null) { if (level == -1) {
Log.w(TAG, "Unknown AVC level: " + levelInteger); Log.w(TAG, "Unknown AVC level: " + levelInteger);
return null; return null;
} }
...@@ -639,7 +639,7 @@ public final class MediaCodecUtil { ...@@ -639,7 +639,7 @@ public final class MediaCodecUtil {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -76,7 +77,7 @@ public final class Metadata implements Parcelable { ...@@ -76,7 +77,7 @@ public final class Metadata implements Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry { ...@@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame { ...@@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
/** /**
...@@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame { ...@@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame { ...@@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame { ...@@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
...@@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame { ...@@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame { ...@@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame { ...@@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
...@@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
...@@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame { ...@@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -140,7 +140,7 @@ public abstract class DownloadAction { ...@@ -140,7 +140,7 @@ public abstract class DownloadAction {
DownloaderConstructorHelper downloaderConstructorHelper); DownloaderConstructorHelper downloaderConstructorHelper);
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (o == null || getClass() != o.getClass()) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }
......
...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; ...@@ -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.DataSource;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
...@@ -250,7 +251,6 @@ public final class DownloadManager { ...@@ -250,7 +251,6 @@ public final class DownloadManager {
Assertions.checkState(!released); Assertions.checkState(!released);
Task task = addTaskForAction(action); Task task = addTaskForAction(action);
if (initialized) { if (initialized) {
notifyListenersTaskStateChange(task);
saveActions(); saveActions();
maybeStartTasks(); maybeStartTasks();
if (task.currentState == STATE_QUEUED) { if (task.currentState == STATE_QUEUED) {
...@@ -413,7 +413,6 @@ public final class DownloadManager { ...@@ -413,7 +413,6 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
logd("Task state is changed", task);
boolean stopped = !task.isActive(); boolean stopped = !task.isActive();
if (stopped) { if (stopped) {
activeDownloadTasks.remove(task); activeDownloadTasks.remove(task);
...@@ -430,6 +429,7 @@ public final class DownloadManager { ...@@ -430,6 +429,7 @@ public final class DownloadManager {
} }
private void notifyListenersTaskStateChange(Task task) { private void notifyListenersTaskStateChange(Task task) {
logd("Task state is changed", task);
TaskState taskState = task.getDownloadState(); TaskState taskState = task.getDownloadState();
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onTaskStateChanged(this, taskState); listener.onTaskStateChanged(this, taskState);
...@@ -468,18 +468,16 @@ public final class DownloadManager { ...@@ -468,18 +468,16 @@ public final class DownloadManager {
listener.onInitialized(DownloadManager.this); listener.onInitialized(DownloadManager.this);
} }
if (!pendingTasks.isEmpty()) { if (!pendingTasks.isEmpty()) {
for (int i = 0; i < pendingTasks.size(); i++) { tasks.addAll(pendingTasks);
tasks.add(pendingTasks.get(i));
}
saveActions(); saveActions();
} }
maybeStartTasks(); maybeStartTasks();
for (int i = 0; i < pendingTasks.size(); i++) { for (int i = 0; i < tasks.size(); i++) {
Task pendingTask = pendingTasks.get(i); Task task = tasks.get(i);
if (pendingTask.currentState == STATE_QUEUED) { if (task.currentState == STATE_QUEUED) {
// Task did not change out of its initial state, and so its initial state // Task did not change out of its initial state, and so its initial state
// won't have been reported to listeners. Do so now. // won't have been reported to listeners. Do so now.
notifyListenersTaskStateChange(pendingTask); notifyListenersTaskStateChange(task);
} }
} }
} }
...@@ -699,9 +697,19 @@ public final class DownloadManager { ...@@ -699,9 +697,19 @@ public final class DownloadManager {
+ ' ' + ' '
+ (action.isRemoveAction ? "remove" : "download") + (action.isRemoveAction ? "remove" : "download")
+ ' ' + ' '
+ toString(action.data)
+ ' '
+ getStateString(); + getStateString();
} }
private static String toString(byte[] data) {
if (data.length > 100) {
return "<data is too long>";
} else {
return '\'' + Util.fromUtf8Bytes(data) + '\'';
}
}
private String getStateString() { private String getStateString() {
switch (currentState) { switch (currentState) {
case STATE_QUEUED_CANCELING: case STATE_QUEUED_CANCELING:
......
...@@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction { ...@@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
......
...@@ -112,7 +112,7 @@ public abstract class SegmentDownloadAction<K extends Comparable<K>> extends Dow ...@@ -112,7 +112,7 @@ public abstract class SegmentDownloadAction<K extends Comparable<K>> extends Dow
protected abstract void writeKey(DataOutputStream output, K key) throws IOException; protected abstract void writeKey(DataOutputStream output, K key) throws IOException;
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
......
...@@ -145,7 +145,7 @@ public interface MediaSource { ...@@ -145,7 +145,7 @@ public interface MediaSource {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable { ...@@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.util.Arrays; import java.util.Arrays;
...@@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable { ...@@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
/** /**
* A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
* supported by this decoder are: * supported by this decoder are:
*
* <ul> * <ul>
* <li>content * <li>content
* <li>core * <li>core
...@@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
* <li>time-clock * <li>time-clock
* <li>time-offset-with-frames * <li>time-offset-with-frames
* <li>time-offset-with-ticks * <li>time-offset-with-ticks
* <li>cell-resolution
* </ul> * </ul>
*
* @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a> * @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
*/ */
public final class TtmlDecoder extends SimpleSubtitleDecoder { public final class TtmlDecoder extends SimpleSubtitleDecoder {
...@@ -74,11 +77,14 @@ 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 FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES = private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); 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 int DEFAULT_FRAME_RATE = 30;
private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE = private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1); new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
private static final CellResolution DEFAULT_CELL_RESOLUTION =
new CellResolution(/* columns= */ 32, /* rows= */ 15);
private final XmlPullParserFactory xmlParserFactory; private final XmlPullParserFactory xmlParserFactory;
...@@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int unsupportedNodeDepth = 0; int unsupportedNodeDepth = 0;
int eventType = xmlParser.getEventType(); int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
while (eventType != XmlPullParser.END_DOCUMENT) { while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peekLast(); TtmlNode parent = nodeStack.peekLast();
if (unsupportedNodeDepth == 0) { if (unsupportedNodeDepth == 0) {
...@@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
if (TtmlNode.TAG_TT.equals(name)) { if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser); frameAndTickRate = parseFrameAndTickRates(xmlParser);
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
} }
if (!isSupportedTag(name)) { if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++; unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) { } else if (TtmlNode.TAG_HEAD.equals(name)) {
parseHeader(xmlParser, globalStyles, regionMap); parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
} else { } else {
try { try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
...@@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
} }
private Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser, private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions) 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<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> globalRegions,
CellResolution cellResolution)
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
do { do {
xmlParser.next(); xmlParser.next();
...@@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style); globalStyles.put(style.getId(), style);
} }
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
if (ttmlRegion != null) { if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion); globalRegions.put(ttmlRegion.id, ttmlRegion);
} }
...@@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
/** /**
* Parses a region declaration. * Parses a region declaration.
* <p> *
* If the region defines an origin and extent, it is required that they're defined as percentages * <p>If the region defines an origin and extent, it is required that they're defined as
* of the viewport. Region declarations that define origin and extent in other formats are * percentages of the viewport. Region declarations that define origin and extent in other formats
* unsupported, and null is returned. * 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); String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) { if (regionId == null) {
return null; return null;
...@@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -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) { private String[] parseStyleIds(String parentStyleIds) {
...@@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.tickRate = tickRate; 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;
}
}
} }
...@@ -175,35 +175,51 @@ import java.util.TreeSet; ...@@ -175,35 +175,51 @@ import java.util.TreeSet;
Map<String, TtmlRegion> regionMap) { Map<String, TtmlRegion> regionMap) {
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>(); TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionOutputs); traverseForText(timeUs, false, regionId, regionOutputs);
traverseForStyle(globalStyles, regionOutputs); traverseForStyle(timeUs, globalStyles, regionOutputs);
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, cues.add(
region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); 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; return cues;
} }
private void traverseForText(long timeUs, boolean descendsPNode, private void traverseForText(
String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) { long timeUs,
boolean descendsPNode,
String inheritedRegion,
Map<String, SpannableStringBuilder> regionOutputs) {
nodeStartsByRegion.clear(); nodeStartsByRegion.clear();
nodeEndsByRegion.clear(); nodeEndsByRegion.clear();
String resolvedRegionId = regionId; if (TAG_METADATA.equals(tag)) {
if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) { // Ignore metadata tag.
resolvedRegionId = inheritedRegion; return;
} }
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
if (isTextNode && descendsPNode) { if (isTextNode && descendsPNode) {
getRegionOutput(resolvedRegionId, regionOutputs).append(text); getRegionOutput(resolvedRegionId, regionOutputs).append(text);
} else if (TAG_BR.equals(tag) && descendsPNode) { } else if (TAG_BR.equals(tag) && descendsPNode) {
getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
} else if (TAG_METADATA.equals(tag)) {
// Do nothing.
} else if (isActive(timeUs)) { } else if (isActive(timeUs)) {
boolean isPNode = TAG_P.equals(tag); // This is a container node, which can contain zero or more children.
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
} }
boolean isPNode = TAG_P.equals(tag);
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId, getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
regionOutputs); regionOutputs);
...@@ -211,39 +227,50 @@ import java.util.TreeSet; ...@@ -211,39 +227,50 @@ import java.util.TreeSet;
if (isPNode) { if (isPNode) {
TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
} }
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
} }
} }
} }
private static SpannableStringBuilder getRegionOutput(String resolvedRegionId, private static SpannableStringBuilder getRegionOutput(
Map<String, SpannableStringBuilder> regionOutputs) { String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {
if (!regionOutputs.containsKey(resolvedRegionId)) { if (!regionOutputs.containsKey(resolvedRegionId)) {
regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
} }
return regionOutputs.get(resolvedRegionId); return regionOutputs.get(resolvedRegionId);
} }
private void traverseForStyle(Map<String, TtmlStyle> globalStyles, private void traverseForStyle(
long timeUs,
Map<String, TtmlStyle> globalStyles,
Map<String, SpannableStringBuilder> regionOutputs) { Map<String, SpannableStringBuilder> regionOutputs) {
if (!isActive(timeUs)) {
return;
}
for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) { for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
String regionId = entry.getKey(); String regionId = entry.getKey();
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue()); int end = entry.getValue();
for (int i = 0; i < getChildCount(); ++i) { if (start != end) {
getChild(i).traverseForStyle(globalStyles, regionOutputs); 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<String, TtmlStyle> globalStyles, private void applyStyleToOutput(
SpannableStringBuilder regionOutput, int start, int end) { Map<String, TtmlStyle> globalStyles,
if (start != end) { SpannableStringBuilder regionOutput,
TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); int start,
if (resolvedStyle != null) { int end) {
TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
} if (resolvedStyle != null) {
TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
} }
} }
......
...@@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue; ...@@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue;
public final String id; public final String id;
public final float position; public final float position;
public final float line; public final float line;
@Cue.LineType public final int lineType; public final @Cue.LineType int lineType;
@Cue.AnchorType public final int lineAnchor; public final @Cue.AnchorType int lineAnchor;
public final float width; public final float width;
public final @Cue.TextSizeType int textSizeType;
public final float textSize;
public TtmlRegion(String id) { 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, public TtmlRegion(
@Cue.AnchorType int lineAnchor, float width) { String id,
float position,
float line,
@Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor,
float width,
int textSizeType,
float textSize) {
this.id = id; this.id = id;
this.position = position; this.position = position;
this.line = line; this.line = line;
this.lineType = lineType; this.lineType = lineType;
this.lineAnchor = lineAnchor; this.lineAnchor = lineAnchor;
this.width = width; this.width = width;
this.textSizeType = textSizeType;
this.textSize = textSize;
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.trackselection; package com.google.android.exoplayer2.trackselection;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
...@@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection { ...@@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -20,6 +20,7 @@ import android.graphics.Point; ...@@ -20,6 +20,7 @@ import android.graphics.Point;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
...@@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
...@@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
...@@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
...@@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { ...@@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.trackselection; package com.google.android.exoplayer2.trackselection;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
/** An array of {@link TrackSelection}s. */ /** An array of {@link TrackSelection}s. */
...@@ -64,7 +65,7 @@ public final class TrackSelectionArray { ...@@ -64,7 +65,7 @@ public final class TrackSelectionArray {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.io.IOException; import java.io.IOException;
...@@ -79,7 +80,7 @@ public interface DataSource { ...@@ -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. * @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. * Closes the source.
......
...@@ -61,7 +61,7 @@ public final class DataSpec { ...@@ -61,7 +61,7 @@ public final class DataSpec {
/** /**
* Body for a POST request, null otherwise. * 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. * The absolute position of the data in the full stream.
*/ */
...@@ -81,12 +81,12 @@ public final class DataSpec { ...@@ -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 * 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. * {@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 * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
* {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. * {@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. * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
...@@ -128,7 +128,8 @@ public final class DataSpec { ...@@ -128,7 +128,8 @@ public final class DataSpec {
* @param key {@link #key}. * @param key {@link #key}.
* @param flags {@link #flags}. * @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); this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
} }
...@@ -143,7 +144,12 @@ public final class DataSpec { ...@@ -143,7 +144,12 @@ public final class DataSpec {
* @param key {@link #key}. * @param key {@link #key}.
* @param flags {@link #flags}. * @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) { @Flags int flags) {
this(uri, null, absoluteStreamPosition, position, length, key, flags); this(uri, null, absoluteStreamPosition, position, length, key, flags);
} }
...@@ -162,7 +168,7 @@ public final class DataSpec { ...@@ -162,7 +168,7 @@ public final class DataSpec {
*/ */
public DataSpec( public DataSpec(
Uri uri, Uri uri,
byte[] postBody, @Nullable byte[] postBody,
long absoluteStreamPosition, long absoluteStreamPosition,
long position, long position,
long length, long length,
...@@ -222,4 +228,13 @@ public final class DataSpec { ...@@ -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);
}
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException; import java.io.IOException;
...@@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource { ...@@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource {
} }
@Override @Override
public Uri getUri() { public @Nullable Uri getUri() {
return upstream.getUri(); return upstream.getUri();
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
...@@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource { ...@@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource {
*/ */
public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
private static final String TAG = "CacheDataSource";
/** /**
* Flags controlling the cache's behavior. * Flags controlling the cache's behavior.
*/ */
...@@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource { ...@@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource {
try { try {
key = CacheUtil.getKey(dataSpec); key = CacheUtil.getKey(dataSpec);
uri = dataSpec.uri; uri = dataSpec.uri;
actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri);
flags = dataSpec.flags; flags = dataSpec.flags;
readPosition = dataSpec.position; readPosition = dataSpec.position;
...@@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource { ...@@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource {
bytesRemaining -= bytesRead; bytesRemaining -= bytesRead;
} }
} else if (currentDataSpecLengthUnset) { } else if (currentDataSpecLengthUnset) {
setNoBytesRemainingAndMaybeStoreLength(); setBytesRemainingAndMaybeStoreLength(0);
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
closeCurrentSource(); closeCurrentSource();
openNextSource(false); openNextSource(false);
...@@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource { ...@@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource {
return bytesRead; return bytesRead;
} catch (IOException e) { } catch (IOException e) {
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
setNoBytesRemainingAndMaybeStoreLength(); setBytesRemainingAndMaybeStoreLength(0);
return C.RESULT_END_OF_INPUT; return C.RESULT_END_OF_INPUT;
} }
handleBeforeThrow(e); handleBeforeThrow(e);
...@@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource { ...@@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource {
currentDataSource = nextDataSource; currentDataSource = nextDataSource;
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
long resolvedLength = nextDataSource.open(nextDataSpec); 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) { if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
bytesRemaining = resolvedLength; setBytesRemainingAndMaybeStoreLength(resolvedLength);
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
}
if (isReadingFromUpstream()) {
actualUri = currentDataSource.getUri();
boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) {
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
}
} }
if (isWritingToCache()) { // TODO find a way to store length and redirected uri in one metadata mutation.
cache.applyContentMetadataMutations(key, mutations); maybeUpdateActualUriFieldAndRedirectedUriMetadata();
}
private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() {
if (!isReadingFromUpstream()) {
return;
} }
actualUri = currentDataSource.getUri();
maybeUpdateRedirectedUriMetadata();
} }
private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { private void maybeUpdateRedirectedUriMetadata() {
bytesRemaining = 0; if (!isWritingToCache()) {
if (isWritingToCache()) { return;
cache.setContentLength(key, readPosition); }
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 static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) {
ContentMetadata contentMetadata = cache.getContentMetadata(key); ContentMetadata contentMetadata = cache.getContentMetadata(key);
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
return redirectedUri == null ? defaultUri : redirectedUri; return redirectedUri == null ? uri : redirectedUri;
} }
private static boolean isCausedByPositionOutOfRange(IOException e) { private static boolean isCausedByPositionOutOfRange(IOException e) {
...@@ -447,6 +458,13 @@ public final class CacheDataSource implements DataSource { ...@@ -447,6 +458,13 @@ public final class CacheDataSource implements DataSource {
return false; return false;
} }
private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException {
this.bytesRemaining = bytesRemaining;
if (isWritingToCache()) {
cache.setContentLength(key, readPosition + bytesRemaining);
}
}
private boolean isReadingFromUpstream() { private boolean isReadingFromUpstream() {
return !isReadingFromCache(); return !isReadingFromCache();
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; 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.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream; import java.io.DataInputStream;
...@@ -236,7 +237,7 @@ import java.util.TreeSet; ...@@ -236,7 +237,7 @@ import java.util.TreeSet;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -26,7 +25,6 @@ import java.io.BufferedInputStream; ...@@ -26,7 +25,6 @@ import java.io.BufferedInputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
...@@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1; private static final int FLAG_ENCRYPTED_INDEX = 1;
private static final String TAG = "CachedContentIndex";
private final HashMap<String, CachedContent> keyToContent; private final HashMap<String, CachedContent> keyToContent;
private final SparseArray<String> idToKey; private final SparseArray<String> idToKey;
private final AtomicFile atomicFile; private final AtomicFile atomicFile;
...@@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec;
add(cachedContent); add(cachedContent);
hashCode += cachedContent.headerHashCode(version); hashCode += cachedContent.headerHashCode(version);
} }
if (input.readInt() != hashCode) { int fileHashCode = input.readInt();
boolean isEOF = input.read() == -1;
if (fileHashCode != hashCode || !isEOF) {
return false; return false;
} }
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error reading cache content index file.", e);
return false; return false;
} finally { } finally {
if (input != null) { if (input != null) {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
...@@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
......
/*
* 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));
}
}
}
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import com.google.android.exoplayer2.C;
/** /**
* Holder for FLAC stream info. * Holder for FLAC stream info.
*/ */
...@@ -52,8 +54,29 @@ public final class FlacStreamInfo { ...@@ -52,8 +54,29 @@ public final class FlacStreamInfo {
// Remaining 16 bytes is md5 value // 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 <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/
public FlacStreamInfo(
int minBlockSize,
int maxBlockSize,
int minFrameSize,
int maxFrameSize,
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples) {
this.minBlockSize = minBlockSize; this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize; this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize; this.minFrameSize = minFrameSize;
...@@ -64,16 +87,43 @@ public final class FlacStreamInfo { ...@@ -64,16 +87,43 @@ public final class FlacStreamInfo {
this.totalSamples = totalSamples; this.totalSamples = totalSamples;
} }
/** Returns the maximum size for a decoded frame from the FLAC stream. */
public int maxDecodedFrameSize() { public int maxDecodedFrameSize() {
return maxBlockSize * channels * (bitsPerSample / 8); return maxBlockSize * channels * (bitsPerSample / 8);
} }
/** Returns the bit-rate of the FLAC stream. */
public int bitRate() { public int bitRate() {
return bitsPerSample * sampleRate; return bitsPerSample * sampleRate;
} }
/** Returns the duration of the FLAC stream in microseconds. */
public long durationUs() { public long durationUs() {
return (totalSamples * 1000000L) / sampleRate; 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;
}
} }
...@@ -144,6 +144,26 @@ public final class UriUtil { ...@@ -144,6 +144,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. * Removes dot segments from the path of a URI.
* *
* @param uri A {@link StringBuilder} containing the URI. * @param uri A {@link StringBuilder} containing the URI.
......
...@@ -17,10 +17,10 @@ package com.google.android.exoplayer2.video; ...@@ -17,10 +17,10 @@ package com.google.android.exoplayer2.video;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
/** /**
...@@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable { ...@@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable {
// Parcelable implementation. // Parcelable implementation.
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
......
...@@ -15,29 +15,29 @@ ...@@ -15,29 +15,29 @@
*/ */
package com.google.android.exoplayer2.video; 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.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay; import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.os.Handler; import android.os.Handler;
import android.os.Handler.Callback; import android.os.Handler.Callback;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Message; import android.os.Message;
import android.support.annotation.IntDef; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.Surface; import android.view.Surface;
import com.google.android.exoplayer2.util.Assertions; 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 com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL10;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A dummy {@link Surface}. * A dummy {@link Surface}.
...@@ -50,16 +50,6 @@ public final class DummySurface extends 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_PROTECTED_CONTENT = "EGL_EXT_protected_content";
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; 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. * Whether the surface is secure.
*/ */
...@@ -161,32 +151,25 @@ public final class DummySurface extends Surface { ...@@ -161,32 +151,25 @@ public final class DummySurface extends Surface {
: SECURE_MODE_PROTECTED_PBUFFER; : SECURE_MODE_PROTECTED_PBUFFER;
} }
private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, private static class DummySurfaceThread extends HandlerThread implements Callback {
Callback {
private static final int MSG_INIT = 1; private static final int MSG_INIT = 1;
private static final int MSG_UPDATE_TEXTURE = 2; private static final int MSG_RELEASE = 2;
private static final int MSG_RELEASE = 3;
private final int[] textureIdHolder;
private EGLDisplay display;
private EGLContext context;
private EGLSurface pbuffer;
private Handler handler;
private SurfaceTexture surfaceTexture;
private Error initError; private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure;
private RuntimeException initException; private @MonotonicNonNull Handler handler;
private DummySurface surface; private @Nullable Error initError;
private @Nullable RuntimeException initException;
private @Nullable DummySurface surface;
public DummySurfaceThread() { public DummySurfaceThread() {
super("dummySurface"); super("dummySurface");
textureIdHolder = new int[1];
} }
public DummySurface init(@SecureMode int secureMode) { public DummySurface init(@SecureMode int secureMode) {
start(); start();
handler = new Handler(getLooper(), this); handler = new Handler(getLooper(), /* callback= */ this);
eglSurfaceTexure = new EGLSurfaceTexture(handler);
boolean wasInterrupted = false; boolean wasInterrupted = false;
synchronized (this) { synchronized (this) {
handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();
...@@ -207,20 +190,16 @@ public final class DummySurface extends Surface { ...@@ -207,20 +190,16 @@ public final class DummySurface extends Surface {
} else if (initError != null) { } else if (initError != null) {
throw initError; throw initError;
} else { } else {
return surface; return Assertions.checkNotNull(surface);
} }
} }
public void release() { public void release() {
Assertions.checkNotNull(handler);
handler.sendEmptyMessage(MSG_RELEASE); handler.sendEmptyMessage(MSG_RELEASE);
} }
@Override @Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
handler.sendEmptyMessage(MSG_UPDATE_TEXTURE);
}
@Override
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case MSG_INIT: case MSG_INIT:
...@@ -238,9 +217,6 @@ public final class DummySurface extends Surface { ...@@ -238,9 +217,6 @@ public final class DummySurface extends Surface {
} }
} }
return true; return true;
case MSG_UPDATE_TEXTURE:
surfaceTexture.updateTexImage();
return true;
case MSG_RELEASE: case MSG_RELEASE:
try { try {
releaseInternal(); releaseInternal();
...@@ -256,103 +232,16 @@ public final class DummySurface extends Surface { ...@@ -256,103 +232,16 @@ public final class DummySurface extends Surface {
} }
private void initInternal(@SecureMode int secureMode) { private void initInternal(@SecureMode int secureMode) {
display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); Assertions.checkNotNull(eglSurfaceTexure);
Assertions.checkState(display != null, "eglGetDisplay failed"); eglSurfaceTexure.init(secureMode);
this.surface =
int[] version = new int[2]; new DummySurface(
boolean eglInitialized = EGL14.eglInitialize(display, version, 0, version, 1); this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
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);
} }
private void releaseInternal() { private void releaseInternal() {
try { Assertions.checkNotNull(eglSurfaceTexure);
if (surfaceTexture != null) { eglSurfaceTexure.release();
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;
}
} }
} }
......
...@@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4006, // https://github.com/google/ExoPlayer/issues/4006,
// https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104. // https://github.com/google/ExoPlayer/issues/4104.
// https://github.com/google/ExoPlayer/issues/4134.
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|| "mido".equals(Util.DEVICE) // Redmi Note 4 || "mido".equals(Util.DEVICE) // Redmi Note 4
...@@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|| "F3311".equals(Util.DEVICE) // Sony Xperia E5 || "F3311".equals(Util.DEVICE) // Sony Xperia E5
|| "M5c".equals(Util.DEVICE) // Meizu M5C || "M5c".equals(Util.DEVICE) // Meizu M5C
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E || "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)) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II
......
...@@ -51,6 +51,7 @@ import java.util.Arrays; ...@@ -51,6 +51,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
...@@ -1813,6 +1814,88 @@ public final class ExoPlayerTest { ...@@ -1813,6 +1814,88 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testCancelMessageBeforeDelivery() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
final AtomicReference<PlayerMessage> 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<PlayerMessage> 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 { public void testSetAndSwitchSurface() throws Exception {
final List<Integer> rendererMessages = new ArrayList<>(); final List<Integer> rendererMessages = new ArrayList<>();
Renderer videoRenderer = Renderer videoRenderer =
...@@ -1934,8 +2017,10 @@ public final class ExoPlayerTest { ...@@ -1934,8 +2017,10 @@ public final class ExoPlayerTest {
@Override @Override
public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { public void handleMessage(SimpleExoPlayer player, int messageType, Object message) {
windowIndex = player.getCurrentWindowIndex(); if (player != null) {
positionMs = player.getCurrentPosition(); windowIndex = player.getCurrentWindowIndex();
positionMs = player.getCurrentPosition();
}
messageCount++; messageCount++;
} }
} }
......
...@@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest { ...@@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest {
} }
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (!(other instanceof EventWindowAndPeriodId)) { if (!(other instanceof EventWindowAndPeriodId)) {
return false; return false;
} }
......
...@@ -15,9 +15,11 @@ ...@@ -15,9 +15,11 @@
*/ */
package com.google.android.exoplayer2.util; 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.android.exoplayer2.util.UriUtil.resolve;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
...@@ -104,4 +106,36 @@ public final class UriUtilTest { ...@@ -104,4 +106,36 @@ public final class UriUtilTest {
assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); 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");
}
} }
...@@ -30,15 +30,11 @@ android { ...@@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }
......
...@@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper { ...@@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper {
manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); 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 @Override
public int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(manifest); Assertions.checkNotNull(manifest);
......
...@@ -30,15 +30,11 @@ android { ...@@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }
......
...@@ -198,24 +198,24 @@ import java.util.List; ...@@ -198,24 +198,24 @@ import java.util.List;
/** /**
* Returns the next chunk to load. * Returns the next chunk to load.
* <p> *
* If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has * <p>If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream
* been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available
* the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to * 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. * contain the {@link HlsUrl} that refers to the playlist that needs refreshing.
* *
* @param previous The most recently loaded media chunk. * @param previous The most recently loaded media chunk.
* @param playbackPositionUs The current playback position in microseconds. If playback of the * @param playbackPositionUs The current playback position relative to the period start in
* period to which this chunk source belongs has not yet started, the value will be the * microseconds. If playback of the period to which this chunk source belongs has not yet
* starting position in the period minus the duration of any media in previous periods still * started, the value will be the starting position in the period minus the duration of any
* to be played. * media in previous periods still to be played.
* @param loadPositionUs The current load position in microseconds. If {@code previous} is null, * @param loadPositionUs The current load position relative to the period start in microseconds.
* this is the starting position from which chunks should be provided. Else it's equal to * If {@code previous} is null, this is the starting position from which chunks should be
* {@code previous.endTimeUs}. * provided. Else it's equal to {@code previous.endTimeUs}.
* @param out A holder to populate. * @param out A holder to populate.
*/ */
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, public void getNextChunk(
HlsChunkHolder out) { HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) {
int oldVariantIndex = previous == null ? C.INDEX_UNSET int oldVariantIndex = previous == null ? C.INDEX_UNSET
: trackGroup.indexOf(previous.trackFormat); : trackGroup.indexOf(previous.trackFormat);
long bufferedDurationUs = loadPositionUs - playbackPositionUs; long bufferedDurationUs = loadPositionUs - playbackPositionUs;
...@@ -261,12 +261,13 @@ import java.util.List; ...@@ -261,12 +261,13 @@ import java.util.List;
// If the playlist is too old to contain the chunk, we need to refresh it. // If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
} else { } else {
// The playlist start time is subtracted from the target position because the segment start long positionOfPlaylistInPeriodUs =
// times are relative to the start of the playlist, but the target position is not. mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs;
chunkMediaSequence = chunkMediaSequence =
Util.binarySearchFloor( Util.binarySearchFloor(
mediaPlaylist.segments, mediaPlaylist.segments,
/* value= */ targetPositionUs - mediaPlaylist.startTimeUs, /* value= */ targetPositionInPlaylistUs,
/* inclusive= */ true, /* inclusive= */ true,
/* stayInBounds= */ !playlistTracker.isLive() || previous == null) /* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ mediaPlaylist.mediaSequence; + mediaPlaylist.mediaSequence;
...@@ -330,9 +331,9 @@ import java.util.List; ...@@ -330,9 +331,9 @@ import java.util.List;
} }
// Compute start time of the next chunk. // Compute start time of the next chunk.
long offsetFromInitialStartTimeUs = long positionOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs; long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
int discontinuitySequence = mediaPlaylist.discontinuitySequence int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ segment.relativeDiscontinuitySequence; + segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
...@@ -352,8 +353,8 @@ import java.util.List; ...@@ -352,8 +353,8 @@ import java.util.List;
muxedCaptionFormats, muxedCaptionFormats,
trackSelection.getSelectionReason(), trackSelection.getSelectionReason(),
trackSelection.getSelectionData(), trackSelection.getSelectionData(),
startTimeUs, segmentStartTimeInPeriodUs,
startTimeUs + segment.durationUs, segmentStartTimeInPeriodUs + segment.durationUs,
chunkMediaSequence, chunkMediaSequence,
discontinuitySequence, discontinuitySequence,
segment.hasGapTag, segment.hasGapTag,
......
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -36,6 +37,11 @@ import java.io.IOException; ...@@ -36,6 +37,11 @@ import java.io.IOException;
sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
} }
public void bindSampleQueue() {
Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
}
public void unbindSampleQueue() { public void unbindSampleQueue() {
if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
...@@ -48,12 +54,11 @@ import java.io.IOException; ...@@ -48,12 +54,11 @@ import java.io.IOException;
@Override @Override
public boolean isReady() { public boolean isReady() {
return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
|| (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex)); || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
} }
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
maybeMapToSampleQueue();
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {
throw new SampleQueueMappingException( throw new SampleQueueMappingException(
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
...@@ -63,22 +68,21 @@ import java.io.IOException; ...@@ -63,22 +68,21 @@ import java.io.IOException;
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
return maybeMapToSampleQueue() return hasValidSampleQueueIndex()
? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat)
: C.RESULT_NOTHING_READ; : C.RESULT_NOTHING_READ;
} }
@Override @Override
public int skipData(long positionUs) { public int skipData(long positionUs) {
return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0; return hasValidSampleQueueIndex()
? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)
: 0;
} }
// Internal methods. // Internal methods.
private boolean maybeMapToSampleQueue() { private boolean hasValidSampleQueueIndex() {
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
}
return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
......
...@@ -102,6 +102,7 @@ import java.util.Arrays; ...@@ -102,6 +102,7 @@ import java.util.Arrays;
private final Runnable maybeFinishPrepareRunnable; private final Runnable maybeFinishPrepareRunnable;
private final Runnable onTracksEndedRunnable; private final Runnable onTracksEndedRunnable;
private final Handler handler; private final Handler handler;
private final ArrayList<HlsSampleStream> hlsSampleStreams;
private SampleQueue[] sampleQueues; private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds; private int[] sampleQueueTrackIds;
...@@ -166,6 +167,7 @@ import java.util.Arrays; ...@@ -166,6 +167,7 @@ import java.util.Arrays;
sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0]; sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>(); mediaChunks = new ArrayList<>();
hlsSampleStreams = new ArrayList<>();
maybeFinishPrepareRunnable = maybeFinishPrepareRunnable =
new Runnable() { new Runnable() {
@Override @Override
...@@ -219,9 +221,6 @@ import java.util.Arrays; ...@@ -219,9 +221,6 @@ import java.util.Arrays;
} }
public int bindSampleQueueToSampleStream(int trackGroupIndex) { public int bindSampleQueueToSampleStream(int trackGroupIndex) {
if (trackGroupToSampleQueueIndex == null) {
return SAMPLE_QUEUE_INDEX_PENDING;
}
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
if (sampleQueueIndex == C.INDEX_UNSET) { if (sampleQueueIndex == C.INDEX_UNSET) {
return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET
...@@ -295,6 +294,9 @@ import java.util.Arrays; ...@@ -295,6 +294,9 @@ import java.util.Arrays;
} }
streams[i] = new HlsSampleStream(this, trackGroupIndex); streams[i] = new HlsSampleStream(this, trackGroupIndex);
streamResetFlags[i] = true; 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 there's still a chance of avoiding a seek, try and seek within the sample queue.
if (sampleQueuesBuilt && !seekRequired) { if (sampleQueuesBuilt && !seekRequired) {
SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]];
...@@ -360,6 +362,7 @@ import java.util.Arrays; ...@@ -360,6 +362,7 @@ import java.util.Arrays;
} }
} }
updateSampleStreams(streams);
seenFirstTrackSelection = true; seenFirstTrackSelection = true;
return seekRequired; return seekRequired;
} }
...@@ -411,6 +414,7 @@ import java.util.Arrays; ...@@ -411,6 +414,7 @@ import java.util.Arrays;
loader.release(this); loader.release(this);
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
released = true; released = true;
hlsSampleStreams.clear();
} }
@Override @Override
...@@ -750,6 +754,15 @@ import java.util.Arrays; ...@@ -750,6 +754,15 @@ import java.util.Arrays;
// Internal methods. // 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) { private boolean finishedReadingChunk(HlsMediaChunk chunk) {
int chunkUid = chunk.uid; int chunkUid = chunk.uid;
int sampleQueueCount = sampleQueues.length; int sampleQueueCount = sampleQueues.length;
...@@ -807,6 +820,9 @@ import java.util.Arrays; ...@@ -807,6 +820,9 @@ import java.util.Arrays;
} }
} }
} }
for (HlsSampleStream sampleStream : hlsSampleStreams) {
sampleStream.bindSampleQueue();
}
} }
/** /**
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls; 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.SampleQueue;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import java.io.IOException; import java.io.IOException;
...@@ -23,7 +24,7 @@ import java.io.IOException; ...@@ -23,7 +24,7 @@ import java.io.IOException;
public final class SampleQueueMappingException extends IOException { public final class SampleQueueMappingException extends IOException {
/** @param mimeType The mime type of the track group whose mapping failed. */ /** @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 + "."); super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + ".");
} }
} }
...@@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper { ...@@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper {
playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); 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 @Override
public int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(playlist); Assertions.checkNotNull(playlist);
......
...@@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/ */
public final long startOffsetUs; 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; public final long startTimeUs;
/** /**
......
...@@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return snapshot; return snapshot;
} }
/** Returns the start time of the first loaded primary playlist. */ /**
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
* media playlist has been loaded.
*/
public long getInitialStartTimeUs() { public long getInitialStartTimeUs() {
return initialStartTimeUs; return initialStartTimeUs;
} }
...@@ -567,7 +570,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -567,7 +570,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal); loadDurationMs, loadable.bytesLoaded(), error, isFatal);
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
boolean shouldRetryIfNotFatal = notifyPlaylistError(playlistUrl, shouldBlacklist); boolean shouldRetryIfNotFatal =
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
if (isFatal) { if (isFatal) {
return Loader.DONT_RETRY_FATAL; return Loader.DONT_RETRY_FATAL;
} }
......
...@@ -30,15 +30,11 @@ android { ...@@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }
......
...@@ -52,6 +52,12 @@ public final class SsDownloadHelper extends DownloadHelper { ...@@ -52,6 +52,12 @@ public final class SsDownloadHelper extends DownloadHelper {
manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri);
} }
/** Returns the SmoothStreaming manifest. Must not be called until after preparation completes. */
public SsManifest getManifest() {
Assertions.checkNotNull(manifest);
return manifest;
}
@Override @Override
public int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(manifest); Assertions.checkNotNull(manifest);
......
...@@ -36,6 +36,7 @@ dependencies { ...@@ -36,6 +36,7 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }
ext { ext {
......
...@@ -25,6 +25,7 @@ import android.graphics.Point; ...@@ -25,6 +25,7 @@ import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
...@@ -44,87 +45,83 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -44,87 +45,83 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* A time bar that shows a current position, buffered position, duration and ad markers. * A time bar that shows a current position, buffered position, duration and ad markers.
* <p> *
* A DefaultTimeBar can be customized by setting attributes, as outlined below. * <p>A DefaultTimeBar can be customized by setting attributes, as outlined below.
* *
* <h3>Attributes</h3> * <h3>Attributes</h3>
*
* The following attributes can be set on a DefaultTimeBar when used in a layout XML file: * The following attributes can be set on a DefaultTimeBar when used in a layout XML file:
*
* <p> * <p>
*
* <ul> * <ul>
* <li><b>{@code bar_height}</b> - Dimension for the height of the time bar. * <li><b>{@code bar_height}</b> - Dimension for the height of the time bar.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}</li> * <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code touch_target_height}</b> - Dimension for the height of the area in which touch * <li><b>{@code touch_target_height}</b> - 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 * interactions with the time bar are handled. If no height is specified, this also determines
* the height of the view. * the height of the view.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}</li> * <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code ad_marker_width}</b> - Dimension for the width of any ad markers shown on the * <li><b>{@code ad_marker_width}</b> - 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. * bar. Ad markers are superimposed on the time bar to show the times at which ads will play.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}</li> * <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber * <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle * handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle
* should be shown. * should be shown.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}</li> * <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber * <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown. * handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}</li> * <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber * <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown. * handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}</li> * <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_drawable}</b> - Optional reference to a drawable to draw for the * <li><b>{@code scrubber_drawable}</b> - 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 * scrubber handle. If set, this overrides the default behavior, which is to draw a circle for
* the scrubber handle. * the scrubber handle.
* </li>
* <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media * <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media
* before the current playback position. * before the current playback position.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}</li> * <li>Corresponding method: {@link #setPlayedColor(int)}
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_color}</b> - Color for the scrubber handle. * <li><b>{@code scrubber_color}</b> - Color for the scrubber handle.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultScrubberColor(int)}</li> * <li>Corresponding method: {@link #setScrubberColor(int)}
* <li>Default: see {@link #getDefaultScrubberColor(int)}
* </ul> * </ul>
* </li>
* <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current * <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current
* played position up to the current buffered position. * played position up to the current buffered position.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultBufferedColor(int)}</li> * <li>Corresponding method: {@link #setBufferedColor(int)}
* <li>Default: see {@link #getDefaultBufferedColor(int)}
* </ul> * </ul>
* </li>
* <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current * <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current
* buffered position. * buffered position.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultUnplayedColor(int)}</li> * <li>Corresponding method: {@link #setUnplayedColor(int)}
* <li>Default: see {@link #getDefaultUnplayedColor(int)}
* </ul> * </ul>
* </li>
* <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers. * <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}</li> * <li>Corresponding method: {@link #setAdMarkerColor(int)}
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}
* </ul> * </ul>
* </li>
* <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers. * <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)}</li> * <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)}
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
* </ul> * </ul>
* </li>
* </ul> * </ul>
*/ */
public class DefaultTimeBar extends View implements TimeBar { public class DefaultTimeBar extends View implements TimeBar {
...@@ -324,6 +321,72 @@ 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 @Override
public void addListener(OnScrubListener listener) { public void addListener(OnScrubListener listener) {
listeners.add(listener); listeners.add(listener);
...@@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar {
update(); update();
} }
// View methods.
@Override @Override
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
super.setEnabled(enabled); super.setEnabled(enabled);
...@@ -408,8 +473,8 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -408,8 +473,8 @@ public class DefaultTimeBar extends View implements TimeBar {
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
if (isInSeekBar(x, y)) { if (isInSeekBar(x, y)) {
startScrubbing();
positionScrubber(x); positionScrubber(x);
startScrubbing();
scrubPosition = getScrubberPosition(); scrubPosition = getScrubberPosition();
update(); update();
invalidate(); invalidate();
......
...@@ -133,6 +133,12 @@ import java.util.List; ...@@ -133,6 +133,12 @@ import java.util.List;
* <li>Corresponding method: {@link #setShutterBackgroundColor(int)} * <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
* <li>Default: {@code unset} * <li>Default: {@code unset}
* </ul> * </ul>
* <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame
* or media artwork is kept visible when the player is reset.
* <ul>
* <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
* <li>Default: {@code false}
* </ul>
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below * <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
* for more details. * for more details.
* <ul> * <ul>
...@@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout { ...@@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout {
private boolean useArtwork; private boolean useArtwork;
private Bitmap defaultArtwork; private Bitmap defaultArtwork;
private boolean showBuffering; private boolean showBuffering;
private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage; private @Nullable CharSequence customErrorMessage;
private int controllerShowTimeoutMs; private int controllerShowTimeoutMs;
...@@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout { ...@@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout {
a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering); showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering);
keepContentOnPlayerReset =
a.getBoolean(
R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
controllerHideDuringAds = controllerHideDuringAds =
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
} finally { } finally {
...@@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout { ...@@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout {
if (useController) { if (useController) {
controller.setPlayer(player); controller.setPlayer(player);
} }
if (shutterView != null) {
shutterView.setVisibility(VISIBLE);
}
if (subtitleView != null) { if (subtitleView != null) {
subtitleView.setCues(null); subtitleView.setCues(null);
} }
updateBuffering(); updateBuffering();
updateErrorMessage(); updateErrorMessage();
updateForCurrentTrackSelections(/* isNewPlayer= */ true);
if (player != null) { if (player != null) {
Player.VideoComponent newVideoComponent = player.getVideoComponent(); Player.VideoComponent newVideoComponent = player.getVideoComponent();
if (newVideoComponent != null) { if (newVideoComponent != null) {
...@@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout { ...@@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout {
} }
player.addListener(componentListener); player.addListener(componentListener);
maybeShowController(false); maybeShowController(false);
updateForCurrentTrackSelections();
} else { } else {
hideController(); hideController();
hideArtwork();
} }
} }
...@@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout { ...@@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout {
Assertions.checkState(!useArtwork || artworkView != null); Assertions.checkState(!useArtwork || artworkView != null);
if (this.useArtwork != useArtwork) { if (this.useArtwork != useArtwork) {
this.useArtwork = useArtwork; this.useArtwork = useArtwork;
updateForCurrentTrackSelections(); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
} }
...@@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout { ...@@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout {
public void setDefaultArtwork(Bitmap defaultArtwork) { public void setDefaultArtwork(Bitmap defaultArtwork) {
if (this.defaultArtwork != defaultArtwork) { if (this.defaultArtwork != defaultArtwork) {
this.defaultArtwork = defaultArtwork; this.defaultArtwork = defaultArtwork;
updateForCurrentTrackSelections(); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
} }
...@@ -601,6 +607,32 @@ public class PlayerView extends FrameLayout { ...@@ -601,6 +607,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)}.
*
* <p>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.
*
* <p>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 * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default. * buffering spinner is not displayed by default.
* *
...@@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout { ...@@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout {
return player != null && player.isPlayingAd() && player.getPlayWhenReady(); return player != null && player.isPlayingAd() && player.getPlayWhenReady();
} }
private void updateForCurrentTrackSelections() { private void updateForCurrentTrackSelections(boolean isNewPlayer) {
if (player == null) { if (player == null || player.getCurrentTrackGroups().isEmpty()) {
if (!keepContentOnPlayerReset) {
hideArtwork();
closeShutter();
}
return; return;
} }
if (isNewPlayer && !keepContentOnPlayerReset) {
// Hide any video from the previous player.
closeShutter();
}
TrackSelectionArray selections = player.getCurrentTrackSelections(); TrackSelectionArray selections = player.getCurrentTrackSelections();
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
...@@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout { ...@@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout {
return; return;
} }
} }
// Video disabled so the shutter must be closed. // Video disabled so the shutter must be closed.
if (shutterView != null) { closeShutter();
shutterView.setVisibility(VISIBLE);
}
// Display artwork if enabled and available, else hide it. // Display artwork if enabled and available, else hide it.
if (useArtwork) { if (useArtwork) {
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
...@@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout { ...@@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout {
} }
} }
private void closeShutter() {
if (shutterView != null) {
shutterView.setVisibility(View.VISIBLE);
}
}
private void updateBuffering() { private void updateBuffering() {
if (bufferingView != null) { if (bufferingView != null) {
boolean showBufferingSpinner = boolean showBufferingSpinner =
...@@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout { ...@@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout {
@Override @Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
updateForCurrentTrackSelections(); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
// Player.EventListener implementation // Player.EventListener implementation
......
...@@ -372,12 +372,22 @@ import com.google.android.exoplayer2.util.Util; ...@@ -372,12 +372,22 @@ import com.google.android.exoplayer2.util.Util;
float previousBottom = layout.getLineTop(0); float previousBottom = layout.getLineTop(0);
int lineCount = layout.getLineCount(); int lineCount = layout.getLineCount();
for (int i = 0; i < lineCount; i++) { for (int i = 0; i < lineCount; i++) {
lineBounds.left = layout.getLineLeft(i) - textPaddingX; float lineTextBoundLeft = layout.getLineLeft(i);
lineBounds.right = layout.getLineRight(i) + textPaddingX; float lineTextBoundRight = layout.getLineRight(i);
lineBounds.left = lineTextBoundLeft - textPaddingX;
lineBounds.right = lineTextBoundRight + textPaddingX;
lineBounds.top = previousBottom; lineBounds.top = previousBottom;
lineBounds.bottom = layout.getLineBottom(i); lineBounds.bottom = layout.getLineBottom(i);
previousBottom = lineBounds.bottom; 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);
}
} }
} }
......
...@@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput {
*/ */
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; 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<SubtitlePainter> painters; private final List<SubtitlePainter> painters;
private List<Cue> cues; private List<Cue> cues;
private int textSizeType; private @Cue.TextSizeType int textSizeType;
private float textSize; private float textSize;
private boolean applyEmbeddedStyles; private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes; private boolean applyEmbeddedFontSizes;
...@@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput {
public SubtitleView(Context context, AttributeSet attrs) { public SubtitleView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
painters = new ArrayList<>(); painters = new ArrayList<>();
textSizeType = FRACTIONAL; textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
textSize = DEFAULT_TEXT_SIZE_FRACTION; textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true; applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true; applyEmbeddedFontSizes = true;
...@@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput {
} else { } else {
resources = context.getResources(); 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 { ...@@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput {
* height after the top and bottom padding has been subtracted. * height after the top and bottom padding has been subtracted.
*/ */
public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) { 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) { if (this.textSizeType == textSizeType && this.textSize == textSize) {
return; return;
} }
...@@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput {
// No space to draw subtitles. // No space to draw subtitles.
return; return;
} }
int rawViewHeight = rawBottom - rawTop;
int viewHeightMinusPadding = bottom - top;
float textSizePx = textSizeType == ABSOLUTE ? textSize float defaultViewTextSizePx =
: textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop)); resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
if (textSizePx <= 0) { if (defaultViewTextSizePx <= 0) {
// Text has no height. // Text has no height.
return; return;
} }
for (int i = 0; i < cueCount; i++) { for (int i = 0; i < cueCount; i++) {
painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, Cue cue = cues.get(i);
textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); 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;
} }
} }
......
...@@ -21,6 +21,8 @@ import android.app.Dialog; ...@@ -21,6 +21,8 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.support.annotation.AttrRes;
import android.support.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair; import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
...@@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout { ...@@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout {
private int rendererIndex; private int rendererIndex;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private boolean isDisabled; 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. * 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 { ...@@ -100,11 +102,13 @@ public class TrackSelectionView extends LinearLayout {
this(context, null); this(context, null);
} }
public TrackSelectionView(Context context, AttributeSet attrs) { public TrackSelectionView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0); 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); super(context, attrs, defStyleAttr);
TypedArray attributeArray = TypedArray attributeArray =
context context
...@@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout { ...@@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout {
* @param allowAdaptiveSelections Whether adaptive selection is enabled. * @param allowAdaptiveSelections Whether adaptive selection is enabled.
*/ */
public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) { public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {
if (!this.allowAdaptiveSelections == allowAdaptiveSelections) { if (this.allowAdaptiveSelections != allowAdaptiveSelections) {
this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowAdaptiveSelections = allowAdaptiveSelections;
updateViews(); updateViews();
} }
...@@ -168,12 +172,14 @@ public class TrackSelectionView extends LinearLayout { ...@@ -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. * @param trackNameProvider The {@link TrackNameProvider} to use.
*/ */
public void setTrackNameProvider(TrackNameProvider trackNameProvider) { public void setTrackNameProvider(TrackNameProvider trackNameProvider) {
this.trackNameProvider = Assertions.checkNotNull(trackNameProvider); this.trackNameProvider = Assertions.checkNotNull(trackNameProvider);
updateViews();
} }
/** /**
...@@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout { ...@@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout {
override = new SelectionOverride(groupIndex, trackIndex); override = new SelectionOverride(groupIndex, trackIndex);
} else { } else {
// An existing override is being modified. // An existing override is being modified.
boolean isEnabled = ((CheckedTextView) view).isChecked();
int overrideLength = override.length; int overrideLength = override.length;
if (isEnabled) { int[] overrideTracks = override.tracks;
if (((CheckedTextView) view).isChecked()) {
// Remove the track from the override. // Remove the track from the override.
if (overrideLength == 1) { if (overrideLength == 1) {
// The last track is being removed, so the override becomes empty. // The last track is being removed, so the override becomes empty.
override = null; override = null;
isDisabled = true; isDisabled = true;
} else { } else {
int[] tracks = getTracksRemoving(override.tracks, trackIndex); int[] tracks = getTracksRemoving(overrideTracks, trackIndex);
override = new SelectionOverride(groupIndex, tracks); override = new SelectionOverride(groupIndex, tracks);
} }
} else { } else {
int[] tracks = getTracksAdding(override.tracks, trackIndex); int[] tracks = getTracksAdding(overrideTracks, trackIndex);
override = new SelectionOverride(groupIndex, tracks); override = new SelectionOverride(groupIndex, tracks);
} }
} }
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<string name="exo_track_selection_title_video">Video</string> <string name="exo_track_selection_title_video">Video</string>
<string name="exo_track_selection_title_audio">Audio</string> <string name="exo_track_selection_title_audio">Audio</string>
<string name="exo_track_selection_title_text">Text</string> <string name="exo_track_selection_title_text">Text</string>
<string name="exo_track_selection_none">Keiner</string> <string name="exo_track_selection_none">Ohne</string>
<string name="exo_track_selection_auto">Automatisch</string> <string name="exo_track_selection_auto">Automatisch</string>
<string name="exo_track_unknown">Unbekannt</string> <string name="exo_track_unknown">Unbekannt</string>
<string name="exo_track_resolution">%1$d × %2$d</string> <string name="exo_track_resolution">%1$d × %2$d</string>
...@@ -31,5 +31,5 @@ ...@@ -31,5 +31,5 @@
<string name="exo_track_surround_5_point_1">5.1-Surround-Sound</string> <string name="exo_track_surround_5_point_1">5.1-Surround-Sound</string>
<string name="exo_track_surround_7_point_1">7.1-Surround-Sound</string> <string name="exo_track_surround_7_point_1">7.1-Surround-Sound</string>
<string name="exo_track_bitrate">%1$.2f Mbit/s</string> <string name="exo_track_bitrate">%1$.2f Mbit/s</string>
<string name="exo_item_list">%1$s und %2$s</string> <string name="exo_item_list">%1$s, %2$s</string>
</resources> </resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment