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

r2.8.1
This commit is contained in:
Andrew Lewis 2018-05-22 12:18:22 +01:00 committed by GitHub
commit 2b55c91af0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 2564 additions and 725 deletions

View File

@ -1,5 +1,38 @@
# 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:

View File

@ -1,19 +0,0 @@
<!-- 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>

View File

@ -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

View File

@ -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"/>

View File

@ -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)) {

View File

@ -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 \

View File

@ -0,0 +1,72 @@
/*
* 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();
}
}

View File

@ -0,0 +1,281 @@
/*
* 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.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
import java.util.Random;
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
public final class FlacExtractorSeekTest extends InstrumentationTestCase {
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
private static final int DURATION_US = 2_741_000;
private static final Uri FILE_URI = Uri.parse("file:///android_asset/" + NO_SEEKTABLE_FLAC);
private static final Random RANDOM = new Random(1234L);
private FakeExtractorOutput expectedOutput;
private FakeTrackOutput expectedTrackOutput;
private DefaultDataSource dataSource;
private PositionHolder positionHolder;
private long totalInputLength;
@Override
protected void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
expectedOutput = new FakeExtractorOutput();
extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC);
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
dataSource =
new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent")
.createDataSource();
totalInputLength = readInputLength();
positionHolder = new PositionHolder();
}
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput());
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = 987_000;
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = seekMap.getDurationUs();
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long firstSeekTimeUs = 987_000;
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
long targetSeekTimeUs = 0;
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long firstSeekTimeUs = 987_000;
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
long targetSeekTimeUs = 1_234_000;
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor();
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long numSeek = 100;
for (long i = 0; i < numSeek; i++) {
long targetSeekTimeUs = RANDOM.nextInt(DURATION_US + 1);
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
}
// Internal methods
private long readInputLength() throws IOException {
DataSpec dataSpec = new DataSpec(FILE_URI, 0, C.LENGTH_UNSET, null);
long totalInputLength = dataSource.open(dataSpec);
Util.closeQuietly(dataSource);
return totalInputLength;
}
/**
* Seeks to the given seek time and keeps reading from input until we can extract at least one
* frame from the seek position, or until end-of-input is reached.
*
* @return The index of the first extracted frame written to the given {@code trackOutput} after
* the seek is completed, or -1 if the seek is completed without any extracted frame.
*/
private int seekToTimeUs(
FlacExtractor flacExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput)
throws IOException, InterruptedException {
int numSampleBeforeSeek = trackOutput.getSampleCount();
SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
long initialSeekLoadPosition = seekPoints.first.position;
flacExtractor.seek(initialSeekLoadPosition, seekTimeUs);
positionHolder.position = C.POSITION_UNSET;
ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition);
int extractorReadResult = Extractor.RESULT_CONTINUE;
while (true) {
try {
// Keep reading until we can read at least one frame after seek
while (extractorReadResult == Extractor.RESULT_CONTINUE
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
extractorReadResult = flacExtractor.read(extractorInput, positionHolder);
}
} finally {
Util.closeQuietly(dataSource);
}
if (extractorReadResult == Extractor.RESULT_SEEK) {
extractorInput = getExtractorInputFromPosition(positionHolder.position);
extractorReadResult = Extractor.RESULT_CONTINUE;
} else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {
return -1;
} else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
// First index after seek = num sample before seek.
return numSampleBeforeSeek;
}
}
}
private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
throws IOException, InterruptedException {
try {
ExtractorInput input = getExtractorInputFromPosition(0);
extractor.init(output);
while (output.seekMap == null) {
extractor.read(input, positionHolder);
}
} finally {
Util.closeQuietly(dataSource);
}
return output.seekMap;
}
private void assertFirstFrameAfterSeekContainTargetSeekTime(
FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {
int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs);
// Assert that after seeking, the first sample frame written to output contains the sample
// at seek time.
trackOutput.assertSample(
firstFrameIndexAfterSeek,
expectedTrackOutput.getSampleData(expectedSampleIndex),
expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),
expectedTrackOutput.getSampleFlags(expectedSampleIndex),
expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));
}
private int findTargetFrameInExpectedOutput(long seekTimeUs) {
List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();
for (int i = 0; i < sampleTimes.size() - 1; i++) {
long currentSampleTime = sampleTimes.get(i);
long nextSampleTime = sampleTimes.get(i + 1);
if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {
return i;
}
}
return sampleTimes.size() - 1;
}
private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {
DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null);
dataSource.open(dataSpec);
return new DefaultExtractorInput(dataSource, position, totalInputLength);
}
private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)
throws IOException, InterruptedException {
byte[] data = TestUtil.getByteArray(context, fileName);
FlacExtractor extractor = new FlacExtractor();
extractor.init(expectedOutput);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {}
}
}

View File

@ -0,0 +1,340 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.flac;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* A {@link SeekMap} implementation for FLAC stream using binary search.
*
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
* frame that contains the target sample.
*/
/* package */ final class FlacBinarySearchSeeker {
/**
* When seeking within the source, if the offset is smaller than or equal to this value, the seek
* operation will be performed using a skip operation. Otherwise, the source will be reloaded at
* the new seek position.
*/
private static final long MAX_SKIP_BYTES = 256 * 1024;
private final FlacStreamInfo streamInfo;
private final FlacBinarySearchSeekMap seekMap;
private final FlacDecoderJni decoderJni;
private final long firstFramePosition;
private final long inputLength;
private final long approxBytesPerFrame;
private @Nullable SeekOperationParams pendingSeekOperationParams;
public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
this.streamInfo = Assertions.checkNotNull(streamInfo);
this.decoderJni = Assertions.checkNotNull(decoderJni);
this.firstFramePosition = firstFramePosition;
this.inputLength = inputLength;
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
pendingSeekOperationParams = null;
seekMap =
new FlacBinarySearchSeekMap(
streamInfo,
firstFramePosition,
inputLength,
streamInfo.durationUs(),
approxBytesPerFrame);
}
/** Returns the seek map for the wrapped FLAC stream. */
public SeekMap getSeekMap() {
return seekMap;
}
/** Sets the target time in microseconds within the stream to seek to. */
public void setSeekTargetUs(long timeUs) {
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
return;
}
pendingSeekOperationParams =
new SeekOperationParams(
timeUs,
streamInfo.getSampleIndex(timeUs),
/* floorSample= */ 0,
/* ceilingSample= */ streamInfo.totalSamples,
/* floorPosition= */ firstFramePosition,
/* ceilingPosition= */ inputLength,
approxBytesPerFrame);
}
/** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
public boolean hasPendingSeek() {
return pendingSeekOperationParams != null;
}
/**
* Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
* {@link Extractor}.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* to hold the position of the required seek.
* @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
* updated to hold the extracted frame that contains the target sample. The caller needs to
* check the byte buffer limit to see if an extracted frame is available.
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public int handlePendingSeek(
ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
throws InterruptedException, IOException {
outputBuffer.position(0);
outputBuffer.limit(0);
while (true) {
long floorPosition = pendingSeekOperationParams.floorPosition;
long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
long searchPosition = pendingSeekOperationParams.nextSearchPosition;
// streamInfo may not contain minFrameSize, in which case this value will be 0.
int minFrameSize = Math.max(1, streamInfo.minFrameSize);
if (floorPosition + minFrameSize >= ceilingPosition) {
// The seeking range is too small for more than 1 frame, so we can just continue from
// the floor position.
pendingSeekOperationParams = null;
decoderJni.reset(floorPosition);
return seekToPosition(input, floorPosition, seekPositionHolder);
}
if (!skipInputUntilPosition(input, searchPosition)) {
return seekToPosition(input, searchPosition, seekPositionHolder);
}
decoderJni.reset(searchPosition);
try {
decoderJni.decodeSampleWithBacktrackPosition(
outputBuffer, /* retryPosition= */ searchPosition);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
// For some reasons, the extractor can't find a frame mid-stream.
// Stop the seeking and let it re-try playing at the last search position.
pendingSeekOperationParams = null;
throw new IOException("Cannot read frame at position " + searchPosition, e);
}
if (outputBuffer.limit() == 0) {
return Extractor.RESULT_END_OF_INPUT;
}
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex();
long nextFrameSamplePosition = decoderJni.getDecodePosition();
boolean targetSampleInLastFrame =
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
if (targetSampleInLastFrame) {
pendingSeekOperationParams = null;
return Extractor.RESULT_CONTINUE;
}
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
} else {
pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
}
}
}
private boolean skipInputUntilPosition(ExtractorInput input, long position)
throws IOException, InterruptedException {
long bytesToSkip = position - input.getPosition();
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
input.skipFully((int) bytesToSkip);
return true;
}
return false;
}
private int seekToPosition(
ExtractorInput input, long position, PositionHolder seekPositionHolder) {
if (position == input.getPosition()) {
return Extractor.RESULT_CONTINUE;
} else {
seekPositionHolder.position = position;
return Extractor.RESULT_SEEK;
}
}
/**
* Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
*
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
* [floorPosition, ceilingPosition).
*/
private static final class SeekOperationParams {
private final long seekTimeUs;
private final long targetSample;
private final long approxBytesPerFrame;
private long floorSample;
private long ceilingSample;
private long floorPosition;
private long ceilingPosition;
private long nextSearchPosition;
private SeekOperationParams(
long seekTimeUs,
long targetSample,
long floorSample,
long ceilingSample,
long floorPosition,
long ceilingPosition,
long approxBytesPerFrame) {
this.seekTimeUs = seekTimeUs;
this.floorSample = floorSample;
this.ceilingSample = ceilingSample;
this.floorPosition = floorPosition;
this.ceilingPosition = ceilingPosition;
this.targetSample = targetSample;
this.approxBytesPerFrame = approxBytesPerFrame;
updateNextSearchPosition();
}
/** Updates the floor constraints (inclusive) of the seek operation. */
private void updateSeekFloor(long floorSample, long floorPosition) {
this.floorSample = floorSample;
this.floorPosition = floorPosition;
updateNextSearchPosition();
}
/** Updates the ceiling constraints (exclusive) of the seek operation. */
private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
this.ceilingSample = ceilingSample;
this.ceilingPosition = ceilingPosition;
updateNextSearchPosition();
}
private void updateNextSearchPosition() {
this.nextSearchPosition =
getNextSearchPosition(
targetSample,
floorSample,
ceilingSample,
floorPosition,
ceilingPosition,
approxBytesPerFrame);
}
/**
* Returns the next position in FLAC stream to search for target sample, given [floorPosition,
* ceilingPosition).
*/
private static long getNextSearchPosition(
long targetSample,
long floorSample,
long ceilingSample,
long floorPosition,
long ceilingPosition,
long approxBytesPerFrame) {
if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
return floorPosition;
}
long samplesToSkip = targetSample - floorSample;
long estimatedBytesPerSample =
Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
// In the stream, the samples are accessed in a group of frame. Given a stream position, the
// seeker will be able to find the first frame following that position.
// Hence, if our target sample is in the middle of a frame, and our estimate position is
// correct, or very near the actual sample position, the seeker will keep accessing the next
// frame, rather than the frame that contains the target sample.
// Moreover, it's better to under-estimate rather than over-estimate, because the extractor
// input can skip forward easily, but cannot rewind easily (it may require a new connection
// to be made).
// Therefore, we should reduce the estimated position by some amount, so it will converge to
// the correct frame earlier.
long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
long confidenceInterval = bytesToSkip / 20;
long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
long estimatedPosition = estimatedFramePosition - confidenceInterval;
return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
}
}
/**
* A {@link SeekMap} implementation that returns the estimated byte location from {@link
* SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
* #getSeekPoints(long)} query.
*/
private static final class FlacBinarySearchSeekMap implements SeekMap {
private final FlacStreamInfo streamInfo;
private final long firstFramePosition;
private final long inputLength;
private final long approxBytesPerFrame;
private final long durationUs;
private FlacBinarySearchSeekMap(
FlacStreamInfo streamInfo,
long firstFramePosition,
long inputLength,
long durationUs,
long approxBytesPerFrame) {
this.streamInfo = streamInfo;
this.firstFramePosition = firstFramePosition;
this.inputLength = inputLength;
this.approxBytesPerFrame = approxBytesPerFrame;
this.durationUs = durationUs;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
long nextSearchPosition =
SeekOperationParams.getNextSearchPosition(
streamInfo.getSampleIndex(timeUs),
/* floorSample= */ 0,
/* ceilingSample= */ streamInfo.totalSamples,
/* floorPosition= */ firstFramePosition,
/* ceilingPosition= */ inputLength,
approxBytesPerFrame);
return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
}
@Override
public long getDurationUs() {
return durationUs;
}
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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");

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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)}.

View File

@ -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) {

View File

@ -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++;

View File

@ -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}

View File

@ -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;
}

View File

@ -63,6 +63,7 @@ public final class PlayerMessage {
private boolean isSent;
private boolean isDelivered;
private boolean isProcessed;
private boolean isCanceled;
/**
* Creates a new message.
@ -242,6 +243,24 @@ public final class PlayerMessage {
return this;
}
/**
* Cancels the message delivery.
*
* @return This message.
* @throws IllegalStateException If this method is called before {@link #send()}.
*/
public synchronized PlayerMessage cancel() {
Assertions.checkState(isSent);
isCanceled = true;
markAsProcessed(/* isDelivered= */ false);
return this;
}
/** Returns whether the message delivery has been canceled. */
public synchronized boolean isCanceled() {
return isCanceled;
}
/**
* Blocks until after the message has been delivered or the player is no longer able to deliver
* the message.

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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() {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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:

View File

@ -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;
}

View File

@ -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;
}

View File

@ -145,7 +145,7 @@ public interface MediaSource {
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -78,6 +78,25 @@ public class Cue {
*/
public static final int LINE_TYPE_NUMBER = 1;
/** The type of default text size for this cue, which may be unset. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TYPE_UNSET,
TEXT_SIZE_TYPE_FRACTIONAL,
TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
TEXT_SIZE_TYPE_ABSOLUTE
})
public @interface TextSizeType {}
/** Text size is measured as a fraction of the viewport size minus the view padding. */
public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0;
/** Text size is measured as a fraction of the viewport size, ignoring the view padding */
public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1;
/** Text size is measured in number of pixels. */
public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2;
/**
* The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated
* with styling spans.
@ -106,40 +125,39 @@ public class Cue {
/**
* The type of the {@link #line} value.
* <p>
* {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
*
* <p>{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
* viewport.
* <p>
* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
* line is taken to be the size of the first line of the cue. When {@link #line} is greater than
* or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from
* the start edge. When {@link #line} is negative lines count from the end of the viewport, with
* -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height
* of the first line of the cue, and the start and end of the viewport are the top and bottom
* respectively.
* <p>
* Note that it's particularly important to consider the effect of {@link #lineAnchor} when using
* {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a
* (potentially multi-line) cue at the very top of the viewport.
* {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue
* at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)}
* and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of
* the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only
* the last line is visible at the top of the viewport.
* {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first
* line is visible at the bottom of the viewport.
*
* <p>{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of
* each line is taken to be the size of the first line of the cue. When {@link #line} is greater
* than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset
* from the start edge. When {@link #line} is negative lines count from the end of the viewport,
* with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the
* height of the first line of the cue, and the start and end of the viewport are the top and
* bottom respectively.
*
* <p>Note that it's particularly important to consider the effect of {@link #lineAnchor} when
* using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)}
* positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 &&
* lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of
* the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 &&
* lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line
* == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible
* at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a
* cue so that only its first line is visible at the bottom of the viewport.
*/
@LineType public final int lineType;
public final @LineType int lineType;
/**
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
* respectively.
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link
* #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
*
* <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
* #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of
* the cue box respectively.
*/
@AnchorType public final int lineAnchor;
public final @AnchorType int lineAnchor;
/**
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
@ -152,14 +170,14 @@ public class Cue {
public final float position;
/**
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* <p>
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
* respectively.
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link
* #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
*
* <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
* #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of
* the cue box respectively.
*/
@AnchorType public final int positionAnchor;
public final @AnchorType int positionAnchor;
/**
* The size of the cue box in the writing direction specified as a fraction of the viewport size
@ -184,6 +202,18 @@ public class Cue {
*/
public final int windowColor;
/**
* The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no
* default text size.
*/
public final @TextSizeType int textSizeType;
/**
* The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default
* text size.
*/
public final float textSize;
/**
* Creates an image cue.
*
@ -194,17 +224,36 @@ public class Cue {
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* @param verticalPosition The position of the vertical anchor within the viewport, expressed as a
* fraction of the viewport height.
* @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START},
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link
* #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
* @param width The width of the cue as a fraction of the viewport width.
* @param height The height of the cue as a fraction of the viewport height, or
* {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the
* specified {@code width}.
* @param height The height of the cue as a fraction of the viewport height, or {@link
* #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified
* {@code width}.
*/
public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor,
float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) {
this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor,
horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK);
public Cue(
Bitmap bitmap,
float horizontalPosition,
@AnchorType int horizontalPositionAnchor,
float verticalPosition,
@AnchorType int verticalPositionAnchor,
float width,
float height) {
this(
/* text= */ null,
/* textAlignment= */ null,
bitmap,
verticalPosition,
/* lineType= */ LINE_TYPE_FRACTION,
verticalPositionAnchor,
horizontalPosition,
horizontalPositionAnchor,
/* textSizeType= */ TYPE_UNSET,
/* textSize= */ DIMEN_UNSET,
width,
height,
/* windowColorSet= */ false,
/* windowColor= */ Color.BLACK);
}
/**
@ -214,7 +263,15 @@ public class Cue {
* @param text See {@link #text}.
*/
public Cue(CharSequence text) {
this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
this(
text,
/* textAlignment= */ null,
/* line= */ DIMEN_UNSET,
/* lineType= */ TYPE_UNSET,
/* lineAnchor= */ TYPE_UNSET,
/* position= */ DIMEN_UNSET,
/* positionAnchor= */ TYPE_UNSET,
/* size= */ DIMEN_UNSET);
}
/**
@ -229,10 +286,68 @@ public class Cue {
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
*/
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) {
this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false,
Color.BLACK);
public Cue(
CharSequence text,
Alignment textAlignment,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
float size) {
this(
text,
textAlignment,
line,
lineType,
lineAnchor,
position,
positionAnchor,
size,
/* windowColorSet= */ false,
/* windowColor= */ Color.BLACK);
}
/**
* Creates a text cue.
*
* @param text See {@link #text}.
* @param textAlignment See {@link #textAlignment}.
* @param line See {@link #line}.
* @param lineType See {@link #lineType}.
* @param lineAnchor See {@link #lineAnchor}.
* @param position See {@link #position}.
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
* @param textSizeType See {@link #textSizeType}.
* @param textSize See {@link #textSize}.
*/
public Cue(
CharSequence text,
Alignment textAlignment,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
float size,
@TextSizeType int textSizeType,
float textSize) {
this(
text,
textAlignment,
/* bitmap= */ null,
line,
lineType,
lineAnchor,
position,
positionAnchor,
textSizeType,
textSize,
size,
/* bitmapHeight= */ DIMEN_UNSET,
/* windowColorSet= */ false,
/* windowColor= */ Color.BLACK);
}
/**
@ -249,16 +364,48 @@ public class Cue {
* @param windowColorSet See {@link #windowColorSet}.
* @param windowColor See {@link #windowColor}.
*/
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
boolean windowColorSet, int windowColor) {
this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size,
DIMEN_UNSET, windowColorSet, windowColor);
public Cue(
CharSequence text,
Alignment textAlignment,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
float size,
boolean windowColorSet,
int windowColor) {
this(
text,
textAlignment,
/* bitmap= */ null,
line,
lineType,
lineAnchor,
position,
positionAnchor,
/* textSizeType= */ TYPE_UNSET,
/* textSize= */ DIMEN_UNSET,
size,
/* bitmapHeight= */ DIMEN_UNSET,
windowColorSet,
windowColor);
}
private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line,
@LineType int lineType, @AnchorType int lineAnchor, float position,
@AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet,
private Cue(
CharSequence text,
Alignment textAlignment,
Bitmap bitmap,
float line,
@LineType int lineType,
@AnchorType int lineAnchor,
float position,
@AnchorType int positionAnchor,
@TextSizeType int textSizeType,
float textSize,
float size,
float bitmapHeight,
boolean windowColorSet,
int windowColor) {
this.text = text;
this.textAlignment = textAlignment;
@ -272,6 +419,8 @@ public class Cue {
this.bitmapHeight = bitmapHeight;
this.windowColorSet = windowColorSet;
this.windowColor = windowColor;
this.textSizeType = textSizeType;
this.textSize = textSize;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource {
*/
public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
private static final String TAG = "CacheDataSource";
/**
* Flags controlling the cache's behavior.
*/
@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource {
try {
key = CacheUtil.getKey(dataSpec);
uri = dataSpec.uri;
actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri);
flags = dataSpec.flags;
readPosition = dataSpec.position;
@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource {
bytesRemaining -= bytesRead;
}
} else if (currentDataSpecLengthUnset) {
setNoBytesRemainingAndMaybeStoreLength();
setBytesRemainingAndMaybeStoreLength(0);
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
closeCurrentSource();
openNextSource(false);
@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource {
return bytesRead;
} catch (IOException e) {
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
setNoBytesRemainingAndMaybeStoreLength();
setBytesRemainingAndMaybeStoreLength(0);
return C.RESULT_END_OF_INPUT;
}
handleBeforeThrow(e);
@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource {
currentDataSource = nextDataSource;
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
long resolvedLength = nextDataSource.open(nextDataSpec);
// Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
ContentMetadataMutations mutations = new ContentMetadataMutations();
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
bytesRemaining = resolvedLength;
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
setBytesRemainingAndMaybeStoreLength(resolvedLength);
}
if (isReadingFromUpstream()) {
actualUri = currentDataSource.getUri();
boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) {
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
}
// TODO find a way to store length and redirected uri in one metadata mutation.
maybeUpdateActualUriFieldAndRedirectedUriMetadata();
}
private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() {
if (!isReadingFromUpstream()) {
return;
}
if (isWritingToCache()) {
actualUri = currentDataSource.getUri();
maybeUpdateRedirectedUriMetadata();
}
private void maybeUpdateRedirectedUriMetadata() {
if (!isWritingToCache()) {
return;
}
ContentMetadataMutations mutations = new ContentMetadataMutations();
boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) {
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
}
try {
cache.applyContentMetadataMutations(key, mutations);
} catch (CacheException e) {
String message =
"Couldn't update redirected URI. "
+ "This might cause relative URIs get resolved incorrectly.";
Log.w(TAG, message, e);
}
}
private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
bytesRemaining = 0;
if (isWritingToCache()) {
cache.setContentLength(key, readPosition);
}
}
private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) {
ContentMetadata contentMetadata = cache.getContentMetadata(key);
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
return redirectedUri == null ? defaultUri : redirectedUri;
return redirectedUri == null ? uri : redirectedUri;
}
private static boolean isCausedByPositionOutOfRange(IOException e) {
@ -447,6 +458,13 @@ public final class CacheDataSource implements DataSource {
return false;
}
private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException {
this.bytesRemaining = bytesRemaining;
if (isWritingToCache()) {
cache.setContentLength(key, readPosition + bytesRemaining);
}
}
private boolean isReadingFromUpstream() {
return !isReadingFromCache();
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -0,0 +1,259 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.os.Handler;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */
@TargetApi(17)
public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable {
/** Secure mode to be used by the EGL surface and context. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
public @interface SecureMode {}
/** No secure EGL surface and context required. */
public static final int SECURE_MODE_NONE = 0;
/** Creating a surfaceless, secured EGL context. */
public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;
/** Creating a secure surface backed by a pixel buffer. */
public static final int SECURE_MODE_PROTECTED_PBUFFER = 2;
private static final int[] EGL_CONFIG_ATTRIBUTES =
new int[] {
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 0,
EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE,
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
EGL14.EGL_NONE
};
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
/** A runtime exception to be thrown if some EGL operations failed. */
public static final class GlException extends RuntimeException {
private GlException(String msg) {
super(msg);
}
}
private final Handler handler;
private final int[] textureIdHolder;
private @Nullable EGLDisplay display;
private @Nullable EGLContext context;
private @Nullable EGLSurface surface;
private @Nullable SurfaceTexture texture;
/**
* @param handler The {@link Handler} that will be used to call {@link
* SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that
* {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s
* looper.
*/
public EGLSurfaceTexture(Handler handler) {
this.handler = handler;
textureIdHolder = new int[1];
}
/**
* Initializes required EGL parameters and creates the {@link SurfaceTexture}.
*
* @param secureMode The {@link SecureMode} to be used for EGL surface.
*/
public void init(@SecureMode int secureMode) {
display = getDefaultDisplay();
EGLConfig config = chooseEGLConfig(display);
context = createEGLContext(display, config, secureMode);
surface = createEGLSurface(display, config, context, secureMode);
generateTextureIds(textureIdHolder);
texture = new SurfaceTexture(textureIdHolder[0]);
texture.setOnFrameAvailableListener(this);
}
/** Releases all allocated resources. */
@SuppressWarnings({"nullness:argument.type.incompatible"})
public void release() {
handler.removeCallbacks(this);
try {
if (texture != null) {
texture.release();
GLES20.glDeleteTextures(1, textureIdHolder, 0);
}
} finally {
if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) {
EGL14.eglDestroySurface(display, surface);
}
if (context != null) {
EGL14.eglDestroyContext(display, context);
}
display = null;
context = null;
surface = null;
texture = null;
}
}
/**
* Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}.
*/
public SurfaceTexture getSurfaceTexture() {
return Assertions.checkNotNull(texture);
}
// SurfaceTexture.OnFrameAvailableListener
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
handler.post(this);
}
// Runnable
@Override
public void run() {
if (texture != null) {
texture.updateTexImage();
}
}
private static EGLDisplay getDefaultDisplay() {
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (display == null) {
throw new GlException("eglGetDisplay failed");
}
int[] version = new int[2];
boolean eglInitialized =
EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);
if (!eglInitialized) {
throw new GlException("eglInitialize failed");
}
return display;
}
private static EGLConfig chooseEGLConfig(EGLDisplay display) {
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
boolean success =
EGL14.eglChooseConfig(
display,
EGL_CONFIG_ATTRIBUTES,
/* attrib_listOffset= */ 0,
configs,
/* configsOffset= */ 0,
/* config_size= */ 1,
numConfigs,
/* num_configOffset= */ 0);
if (!success || numConfigs[0] <= 0 || configs[0] == null) {
throw new GlException(
Util.formatInvariant(
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
success, numConfigs[0], configs[0]));
}
return configs[0];
}
private static EGLContext createEGLContext(
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
int[] glAttributes;
if (secureMode == SECURE_MODE_NONE) {
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
} else {
glAttributes =
new int[] {
EGL14.EGL_CONTEXT_CLIENT_VERSION,
2,
EGL_PROTECTED_CONTENT_EXT,
EGL14.EGL_TRUE,
EGL14.EGL_NONE
};
}
EGLContext context =
EGL14.eglCreateContext(
display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
if (context == null) {
throw new GlException("eglCreateContext failed");
}
return context;
}
private static EGLSurface createEGLSurface(
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
EGLSurface surface;
if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
surface = EGL14.EGL_NO_SURFACE;
} else {
int[] pbufferAttributes;
if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) {
pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH,
1,
EGL14.EGL_HEIGHT,
1,
EGL_PROTECTED_CONTENT_EXT,
EGL14.EGL_TRUE,
EGL14.EGL_NONE
};
} else {
pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH, 1,
EGL14.EGL_HEIGHT, 1,
EGL14.EGL_NONE
};
}
surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
if (surface == null) {
throw new GlException("eglCreatePbufferSurface failed");
}
}
boolean eglMadeCurrent =
EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
if (!eglMadeCurrent) {
throw new GlException("eglMakeCurrent failed");
}
return surface;
}
private static void generateTextureIds(int[] textureIdHolder) {
GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
int errorCode = GLES20.glGetError();
if (errorCode != GLES20.GL_NO_ERROR) {
throw new GlException("glGenTextures failed. Error: " + Integer.toHexString(errorCode));
}
}
}

View File

@ -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;
}
}

View File

@ -143,6 +143,26 @@ public final class UriUtil {
}
}
/**
* Removes query parameter from an Uri, if present.
*
* @param uri The uri.
* @param queryParameterName The name of the query parameter.
* @return The uri without the query parameter.
*/
public static Uri removeQueryParameter(Uri uri, String queryParameterName) {
Uri.Builder builder = uri.buildUpon();
builder.clearQuery();
for (String key : uri.getQueryParameterNames()) {
if (!key.equals(queryParameterName)) {
for (String value : uri.getQueryParameters(key)) {
builder.appendQueryParameter(key, value);
}
}
}
return builder.build();
}
/**
* Removes dot segments from the path of a URI.
*

View File

@ -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;
}

View File

@ -15,29 +15,29 @@
*/
package com.google.android.exoplayer2.video;
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE;
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER;
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Surface;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EGLSurfaceTexture;
import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.microedition.khronos.egl.EGL10;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A dummy {@link Surface}.
@ -50,16 +50,6 @@ public final class DummySurface extends Surface {
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
private @interface SecureMode {}
private static final int SECURE_MODE_NONE = 0;
private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;
private static final int SECURE_MODE_PROTECTED_PBUFFER = 2;
/**
* Whether the surface is secure.
*/
@ -161,32 +151,25 @@ public final class DummySurface extends Surface {
: SECURE_MODE_PROTECTED_PBUFFER;
}
private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
Callback {
private static class DummySurfaceThread extends HandlerThread implements Callback {
private static final int MSG_INIT = 1;
private static final int MSG_UPDATE_TEXTURE = 2;
private static final int MSG_RELEASE = 3;
private static final int MSG_RELEASE = 2;
private final int[] textureIdHolder;
private EGLDisplay display;
private EGLContext context;
private EGLSurface pbuffer;
private Handler handler;
private SurfaceTexture surfaceTexture;
private Error initError;
private RuntimeException initException;
private DummySurface surface;
private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure;
private @MonotonicNonNull Handler handler;
private @Nullable Error initError;
private @Nullable RuntimeException initException;
private @Nullable DummySurface surface;
public DummySurfaceThread() {
super("dummySurface");
textureIdHolder = new int[1];
}
public DummySurface init(@SecureMode int secureMode) {
start();
handler = new Handler(getLooper(), this);
handler = new Handler(getLooper(), /* callback= */ this);
eglSurfaceTexure = new EGLSurfaceTexture(handler);
boolean wasInterrupted = false;
synchronized (this) {
handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();
@ -207,19 +190,15 @@ public final class DummySurface extends Surface {
} else if (initError != null) {
throw initError;
} else {
return surface;
return Assertions.checkNotNull(surface);
}
}
public void release() {
Assertions.checkNotNull(handler);
handler.sendEmptyMessage(MSG_RELEASE);
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
handler.sendEmptyMessage(MSG_UPDATE_TEXTURE);
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
@ -238,9 +217,6 @@ public final class DummySurface extends Surface {
}
}
return true;
case MSG_UPDATE_TEXTURE:
surfaceTexture.updateTexImage();
return true;
case MSG_RELEASE:
try {
releaseInternal();
@ -256,103 +232,16 @@ public final class DummySurface extends Surface {
}
private void initInternal(@SecureMode int secureMode) {
display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
Assertions.checkState(display != null, "eglGetDisplay failed");
int[] version = new int[2];
boolean eglInitialized = EGL14.eglInitialize(display, version, 0, version, 1);
Assertions.checkState(eglInitialized, "eglInitialize failed");
int[] eglAttributes =
new int[] {
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 0,
EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE,
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
boolean eglChooseConfigSuccess =
EGL14.eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, numConfigs, 0);
Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null,
"eglChooseConfig failed");
EGLConfig config = configs[0];
int[] glAttributes;
if (secureMode == SECURE_MODE_NONE) {
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
} else {
glAttributes =
new int[] {
EGL14.EGL_CONTEXT_CLIENT_VERSION,
2,
EGL_PROTECTED_CONTENT_EXT,
EGL14.EGL_TRUE,
EGL14.EGL_NONE
};
}
context =
EGL14.eglCreateContext(
display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
Assertions.checkState(context != null, "eglCreateContext failed");
EGLSurface surface;
if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
surface = EGL14.EGL_NO_SURFACE;
} else {
int[] pbufferAttributes;
if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) {
pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH,
1,
EGL14.EGL_HEIGHT,
1,
EGL_PROTECTED_CONTENT_EXT,
EGL14.EGL_TRUE,
EGL14.EGL_NONE
};
} else {
pbufferAttributes = new int[] {EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE};
}
pbuffer = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, 0);
Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed");
surface = pbuffer;
}
boolean eglMadeCurrent = EGL14.eglMakeCurrent(display, surface, surface, context);
Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed");
GLES20.glGenTextures(1, textureIdHolder, 0);
surfaceTexture = new SurfaceTexture(textureIdHolder[0]);
surfaceTexture.setOnFrameAvailableListener(this);
this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE);
Assertions.checkNotNull(eglSurfaceTexure);
eglSurfaceTexure.init(secureMode);
this.surface =
new DummySurface(
this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
}
private void releaseInternal() {
try {
if (surfaceTexture != null) {
surfaceTexture.release();
GLES20.glDeleteTextures(1, textureIdHolder, 0);
}
} finally {
if (pbuffer != null) {
EGL14.eglDestroySurface(display, pbuffer);
}
if (context != null) {
EGL14.eglDestroyContext(display, context);
}
pbuffer = null;
context = null;
display = null;
surface = null;
surfaceTexture = null;
}
Assertions.checkNotNull(eglSurfaceTexure);
eglSurfaceTexure.release();
}
}

View File

@ -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

View File

@ -51,6 +51,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -1812,6 +1813,88 @@ public final class ExoPlayerTest {
assertThat(target3.windowIndex).isEqualTo(2);
}
@Test
public void testCancelMessageBeforeDelivery() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
final AtomicReference<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<>();
@ -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++;
}
}

View File

@ -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;
}

View File

@ -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");
}
}

View File

@ -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')
}

View File

@ -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);

View File

@ -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')
}

View File

@ -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,

View File

@ -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;

View File

@ -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();
}
}
/**

View File

@ -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 + ".");
}
}

View File

@ -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);

View File

@ -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;
/**

View File

@ -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;
}

View File

@ -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')
}

View File

@ -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);

View File

@ -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 {

View File

@ -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();

View File

@ -50,6 +50,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* A notification manager to start, update and cancel a media style notification reflecting the
@ -205,7 +206,9 @@ public class PlayerNotificationManager {
new Runnable() {
@Override
public void run() {
if (notificationTag == currentNotificationTag && isNotificationStarted) {
if (player != null
&& notificationTag == currentNotificationTag
&& isNotificationStarted) {
updateNotification(bitmap);
}
}
@ -260,7 +263,7 @@ public class PlayerNotificationManager {
private final String channelId;
private final int notificationId;
private final MediaDescriptionAdapter mediaDescriptionAdapter;
private final CustomActionReceiver customActionReceiver;
private final @Nullable CustomActionReceiver customActionReceiver;
private final Handler mainHandler;
private final NotificationManagerCompat notificationManager;
private final IntentFilter intentFilter;
@ -269,12 +272,12 @@ public class PlayerNotificationManager {
private final Map<String, NotificationCompat.Action> playbackActions;
private final Map<String, NotificationCompat.Action> customActions;
private Player player;
private @Nullable Player player;
private ControlDispatcher controlDispatcher;
private boolean isNotificationStarted;
private int currentNotificationTag;
private NotificationListener notificationListener;
private MediaSessionCompat.Token mediaSessionToken;
private @Nullable NotificationListener notificationListener;
private @Nullable MediaSessionCompat.Token mediaSessionToken;
private boolean useNavigationActions;
private boolean usePlayPauseActions;
private @Nullable String stopAction;
@ -365,6 +368,20 @@ public class PlayerNotificationManager {
playerListener = new PlayerListener();
notificationBroadcastReceiver = new NotificationBroadcastReceiver();
intentFilter = new IntentFilter();
useNavigationActions = true;
usePlayPauseActions = true;
ongoing = true;
colorized = true;
useChronometer = true;
color = Color.TRANSPARENT;
smallIconResourceId = R.drawable.exo_notification_small_icon;
defaults = 0;
priority = NotificationCompat.PRIORITY_LOW;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
rewindMs = DEFAULT_REWIND_MS;
stopAction = ACTION_STOP;
badgeIconType = NotificationCompat.BADGE_ICON_SMALL;
visibility = NotificationCompat.VISIBILITY_PUBLIC;
// initialize actions
playbackActions = createPlaybackActions(context);
@ -378,22 +395,7 @@ public class PlayerNotificationManager {
for (String action : customActions.keySet()) {
intentFilter.addAction(action);
}
setStopAction(ACTION_STOP);
useNavigationActions = true;
usePlayPauseActions = true;
ongoing = true;
colorized = true;
useChronometer = true;
color = Color.TRANSPARENT;
smallIconResourceId = R.drawable.exo_notification_small_icon;
defaults = 0;
priority = NotificationCompat.PRIORITY_LOW;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
rewindMs = DEFAULT_REWIND_MS;
setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);
setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
}
/**
@ -512,10 +514,9 @@ public class PlayerNotificationManager {
}
this.stopAction = stopAction;
if (ACTION_STOP.equals(stopAction)) {
stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent;
stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
} else if (stopAction != null) {
Assertions.checkArgument(customActions.containsKey(stopAction));
stopPendingIntent = customActions.get(stopAction).actionIntent;
stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent;
} else {
stopPendingIntent = null;
}
@ -698,25 +699,28 @@ public class PlayerNotificationManager {
maybeUpdateNotification();
}
private Notification updateNotification(Bitmap bitmap) {
@RequiresNonNull("player")
private Notification updateNotification(@Nullable Bitmap bitmap) {
Notification notification = createNotification(player, bitmap);
notificationManager.notify(notificationId, notification);
return notification;
}
private void startOrUpdateNotification() {
Notification notification = updateNotification(null);
if (!isNotificationStarted) {
isNotificationStarted = true;
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
if (notificationListener != null) {
notificationListener.onNotificationStarted(notificationId, notification);
if (player != null) {
Notification notification = updateNotification(null);
if (!isNotificationStarted) {
isNotificationStarted = true;
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
if (notificationListener != null) {
notificationListener.onNotificationStarted(notificationId, notification);
}
}
}
}
private void maybeUpdateNotification() {
if (isNotificationStarted) {
if (isNotificationStarted && player != null) {
updateNotification(null);
}
}
@ -732,64 +736,6 @@ public class PlayerNotificationManager {
}
}
private Map<String, NotificationCompat.Action> createPlaybackActions(Context context) {
Map<String, NotificationCompat.Action> actions = new HashMap<>();
Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName());
actions.put(
ACTION_PLAY,
new NotificationCompat.Action(
R.drawable.exo_notification_play,
context.getString(R.string.exo_controls_play_description),
PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName());
actions.put(
ACTION_PAUSE,
new NotificationCompat.Action(
R.drawable.exo_notification_pause,
context.getString(R.string.exo_controls_pause_description),
PendingIntent.getBroadcast(
context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName());
actions.put(
ACTION_STOP,
new NotificationCompat.Action(
R.drawable.exo_notification_stop,
context.getString(R.string.exo_controls_stop_description),
PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName());
actions.put(
ACTION_REWIND,
new NotificationCompat.Action(
R.drawable.exo_notification_rewind,
context.getString(R.string.exo_controls_rewind_description),
PendingIntent.getBroadcast(
context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName());
actions.put(
ACTION_FAST_FORWARD,
new NotificationCompat.Action(
R.drawable.exo_notification_fastforward,
context.getString(R.string.exo_controls_fastforward_description),
PendingIntent.getBroadcast(
context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName());
actions.put(
ACTION_PREVIOUS,
new NotificationCompat.Action(
R.drawable.exo_notification_previous,
context.getString(R.string.exo_controls_previous_description),
PendingIntent.getBroadcast(
context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName());
actions.put(
ACTION_NEXT,
new NotificationCompat.Action(
R.drawable.exo_notification_next,
context.getString(R.string.exo_controls_next_description),
PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
return actions;
}
/**
* Creates the notification given the current player state.
*
@ -821,7 +767,7 @@ public class PlayerNotificationManager {
// Configure stop action (eg. when user dismisses the notification when !isOngoing).
boolean useStopAction = stopAction != null && !isPlayingAd;
mediaStyle.setShowCancelButton(useStopAction);
if (useStopAction) {
if (useStopAction && stopPendingIntent != null) {
builder.setDeleteIntent(stopPendingIntent);
mediaStyle.setCancelButtonIntent(stopPendingIntent);
}
@ -905,7 +851,7 @@ public class PlayerNotificationManager {
if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) {
stringActions.add(ACTION_NEXT);
}
if (!customActions.isEmpty()) {
if (customActionReceiver != null) {
stringActions.addAll(customActionReceiver.getCustomActions(player));
}
if (ACTION_STOP.equals(stopAction)) {
@ -932,6 +878,64 @@ public class PlayerNotificationManager {
return new int[] {actionIndex};
}
private static Map<String, NotificationCompat.Action> createPlaybackActions(Context context) {
Map<String, NotificationCompat.Action> actions = new HashMap<>();
Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName());
actions.put(
ACTION_PLAY,
new NotificationCompat.Action(
R.drawable.exo_notification_play,
context.getString(R.string.exo_controls_play_description),
PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName());
actions.put(
ACTION_PAUSE,
new NotificationCompat.Action(
R.drawable.exo_notification_pause,
context.getString(R.string.exo_controls_pause_description),
PendingIntent.getBroadcast(
context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName());
actions.put(
ACTION_STOP,
new NotificationCompat.Action(
R.drawable.exo_notification_stop,
context.getString(R.string.exo_controls_stop_description),
PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName());
actions.put(
ACTION_REWIND,
new NotificationCompat.Action(
R.drawable.exo_notification_rewind,
context.getString(R.string.exo_controls_rewind_description),
PendingIntent.getBroadcast(
context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName());
actions.put(
ACTION_FAST_FORWARD,
new NotificationCompat.Action(
R.drawable.exo_notification_fastforward,
context.getString(R.string.exo_controls_fastforward_description),
PendingIntent.getBroadcast(
context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName());
actions.put(
ACTION_PREVIOUS,
new NotificationCompat.Action(
R.drawable.exo_notification_previous,
context.getString(R.string.exo_controls_previous_description),
PendingIntent.getBroadcast(
context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName());
actions.put(
ACTION_NEXT,
new NotificationCompat.Action(
R.drawable.exo_notification_next,
context.getString(R.string.exo_controls_next_description),
PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
return actions;
}
private class PlayerListener extends Player.DefaultEventListener {
@Override
@ -946,7 +950,7 @@ public class PlayerNotificationManager {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification();
@ -954,7 +958,7 @@ public class PlayerNotificationManager {
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification();
@ -967,7 +971,7 @@ public class PlayerNotificationManager {
@Override
public void onRepeatModeChanged(int repeatMode) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification();
@ -985,7 +989,8 @@ public class PlayerNotificationManager {
@Override
public void onReceive(Context context, Intent intent) {
if (!isNotificationStarted) {
Player player = PlayerNotificationManager.this.player;
if (player == null || !isNotificationStarted) {
return;
}
String action = intent.getAction();
@ -1013,7 +1018,7 @@ public class PlayerNotificationManager {
} else if (ACTION_STOP.equals(action)) {
controlDispatcher.dispatchStop(player, true);
stopNotification();
} else if (customActions.containsKey(action)) {
} else if (customActionReceiver != null && customActions.containsKey(action)) {
customActionReceiver.onCustomAction(player, action, intent);
}
}

View File

@ -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);
}
}
@ -600,6 +606,32 @@ public class PlayerView extends FrameLayout {
}
}
/**
* Sets whether the currently displayed video frame or media artwork is kept visible when the
* player is reset. A player reset is defined to mean the player being re-prepared with different
* media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being
* replaced or cleared by calling {@link #setPlayer(Player)}.
*
* <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

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More