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