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 # # Release notes #
### 2.8.1 ###
* HLS:
* Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
([#4239](https://github.com/google/ExoPlayer/issues/4239)).
* Fix playback of clipped streams starting from non-keyframe positions
([#4241](https://github.com/google/ExoPlayer/issues/4241)).
* OkHttp extension: Fix to correctly include response headers in thrown
`InvalidResponseCodeException`s.
* Add possibility to cancel `PlayerMessage`s.
* UI components:
* Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed
video frame or media artwork visible when the player is reset
([#2843](https://github.com/google/ExoPlayer/issues/2843)).
* Fix crash when switching surface on Moto E(4)
([#4134](https://github.com/google/ExoPlayer/issues/4134)).
* Fix a bug that could cause event listeners to be called with inconsistent
information if an event listener interacted with the player
([#4262](https://github.com/google/ExoPlayer/issues/4262)).
* Audio:
* Fix extraction of PCM in MP4/MOV
([#4228](https://github.com/google/ExoPlayer/issues/4228)).
* FLAC: Supports seeking for FLAC files without SEEKTABLE
([#1808](https://github.com/google/ExoPlayer/issues/1808)).
* Captions:
* TTML:
* Fix a styling issue when there are multiple regions displayed at the same
time that can make text size of each region much smaller than defined.
* Fix an issue when the caption line has no text (empty line or only line
break), and the line's background is still displayed.
* Support TTML font size using % correctly (as percentage of document cell
resolution).
### 2.8.0 ### ### 2.8.0 ###
* Downloading: * Downloading:
@ -75,7 +108,7 @@
* Allow multiple listeners for `DefaultDrmSessionManager`. * Allow multiple listeners for `DefaultDrmSessionManager`.
* Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.
* Change minimum API requirement for CBC and pattern encryption from 24 to 25 * Change minimum API requirement for CBC and pattern encryption from 24 to 25
([#4022][https://github.com/google/ExoPlayer/issues/4022]). ([#4022](https://github.com/google/ExoPlayer/issues/4022)).
* Fix handling of 307/308 redirects when making license requests * Fix handling of 307/308 redirects when making license requests
([#4108](https://github.com/google/ExoPlayer/issues/4108)). ([#4108](https://github.com/google/ExoPlayer/issues/4108)).
* HLS: * HLS:

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. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.8.0' releaseVersion = '2.8.1'
releaseVersionCode = 2800 releaseVersionCode = 2801
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
@ -33,6 +33,7 @@ project.ext {
robolectricVersion = '3.7.1' robolectricVersion = '3.7.1'
autoValueVersion = '1.6' autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.0.2'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix

View File

@ -18,6 +18,7 @@
package="com.google.android.exoplayer2.demo"> package="com.google.android.exoplayer2.demo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.Nullable;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
@ -110,7 +111,7 @@ import java.util.Map;
// equals and hashCode implementations. // equals and hashCode implementations.
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (this == other) { if (this == other) {
return true; return true;
} else if (!(other instanceof CastTimeline)) { } else if (!(other instanceof CastTimeline)) {

View File

@ -70,7 +70,8 @@ COMMON_OPTIONS="\
--enable-decoder=flac \ --enable-decoder=flac \
" && \ " && \
cd "${FFMPEG_EXT_PATH}/jni" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && \
./configure \ ./configure \
--libdir=android-libs/armeabi-v7a \ --libdir=android-libs/armeabi-v7a \
--arch=arm \ --arch=arm \

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); decoderJni.setData(inputBuffer.data);
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
int result;
try { try {
result = decoderJni.decodeSample(outputData); decoderJni.decodeSample(outputData);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
return new FlacDecoderException("Frame decoding failed", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (result < 0) {
return new FlacDecoderException("Frame decoding failed");
}
outputData.position(0);
outputData.limit(result);
return null; return null;
} }

View File

@ -26,6 +26,17 @@ import java.nio.ByteBuffer;
*/ */
/* package */ final class FlacDecoderJni { /* package */ final class FlacDecoderJni {
/** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */
public static final class FlacFrameDecodeException extends Exception {
public final int errorCode;
public FlacFrameDecodeException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
}
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private final long nativeDecoderContext; private final long nativeDecoderContext;
@ -116,14 +127,50 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); return flacDecodeMetadata(nativeDecoderContext);
} }
public int decodeSample(ByteBuffer output) throws IOException, InterruptedException { /**
return output.isDirect() * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
* 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) ? flacDecodeToBuffer(nativeDecoderContext, output)
: flacDecodeToArray(nativeDecoderContext, output.array()); : 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); return flacGetDecodePosition(nativeDecoderContext);
} }
public long getLastSampleTimestamp() { /** Returns the timestamp for the first sample in the last decoded frame. */
return flacGetLastTimestamp(nativeDecoderContext); public long getLastFrameTimestamp() {
return flacGetLastFrameTimestamp(nativeDecoderContext);
}
/** Returns the first sample index of the last extracted frame. */
public long getLastFrameFirstSampleIndex() {
return flacGetLastFrameFirstSampleIndex(nativeDecoderContext);
}
/** Returns the first sample index of the frame to be extracted next. */
public long getNextFrameFirstSampleIndex() {
return flacGetNextFrameFirstSampleIndex(nativeDecoderContext);
} }
/** /**
@ -153,6 +211,11 @@ import java.nio.ByteBuffer;
return flacGetStateString(nativeDecoderContext); return flacGetStateString(nativeDecoderContext);
} }
/** Returns whether the decoder has read to the end of the input. */
public boolean isDecoderAtEndOfInput() {
return flacIsDecoderAtEndOfStream(nativeDecoderContext);
}
public void flush() { public void flush() {
flacFlush(nativeDecoderContext); flacFlush(nativeDecoderContext);
} }
@ -181,18 +244,34 @@ import java.nio.ByteBuffer;
} }
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamInfo flacDecodeMetadata(long context)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToArray(long context, byte[] outputArray) private native int flacDecodeToArray(long context, byte[] outputArray)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native long flacGetDecodePosition(long context); private native long flacGetDecodePosition(long context);
private native long flacGetLastTimestamp(long context);
private native long flacGetLastFrameTimestamp(long context);
private native long flacGetLastFrameFirstSampleIndex(long context);
private native long flacGetNextFrameFirstSampleIndex(long context);
private native long flacGetSeekPosition(long context, long timeUs); private native long flacGetSeekPosition(long context, long timeUs);
private native String flacGetStateString(long context); private native String flacGetStateString(long context);
private native boolean flacIsDecoderAtEndOfStream(long context);
private native void flacFlush(long context); private native void flacFlush(long context);
private native void flacReset(long context, long newPosition); private native void flacReset(long context, long newPosition);
private native void flacRelease(long context); private native void flacRelease(long context);
} }

View File

@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor {
private ParsableByteArray outputBuffer; private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer; private ByteBuffer outputByteBuffer;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata; private Metadata id3Metadata;
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
private boolean metadataParsed; private boolean readPastStreamInfo;
/** Constructs an instance with flags = 0. */ /** Constructs an instance with flags = 0. */
public FlacExtractor() { public FlacExtractor() {
@ -136,83 +138,43 @@ public final class FlacExtractor implements Extractor {
} }
decoderJni.setData(input); decoderJni.setData(input);
readPastStreamInfo(input);
if (!metadataParsed) { if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
final FlacStreamInfo streamInfo; return handlePendingSeek(input, seekPosition);
try {
streamInfo = decoderJni.decodeMetadata();
if (streamInfo == null) {
throw new IOException("Metadata decoding failed");
}
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
throw e; // never executes
}
metadataParsed = true;
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
extractorOutput.seekMap(
isSeekable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
streamInfo.bitRate(),
streamInfo.maxDecodedFrameSize(),
streamInfo.channels,
streamInfo.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
} }
outputBuffer.reset();
long lastDecodePosition = decoderJni.getDecodePosition(); long lastDecodePosition = decoderJni.getDecodePosition();
int size;
try { try {
size = decoderJni.decodeSample(outputByteBuffer); decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (IOException e) { } catch (FlacDecoderJni.FlacFrameDecodeException e) {
if (lastDecodePosition >= 0) { throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
decoderJni.reset(lastDecodePosition);
input.setRetryPosition(lastDecodePosition, e);
} }
throw e; int outputSize = outputByteBuffer.limit();
} if (outputSize == 0) {
if (size <= 0) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
trackOutput.sampleData(outputBuffer, size);
trackOutput.sampleMetadata(decoderJni.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size,
0, null);
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
} }
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
metadataParsed = false; readPastStreamInfo = false;
} }
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.reset(position); decoderJni.reset(position);
} }
if (flacBinarySearchSeeker != null) {
flacBinarySearchSeeker.setSeekTargetUs(timeUs);
}
} }
@Override @Override
public void release() { public void release() {
flacBinarySearchSeeker = null;
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.release(); decoderJni.release();
decoderJni = null; decoderJni = null;
@ -244,6 +206,100 @@ public final class FlacExtractor implements Extractor {
return Arrays.equals(header, FLAC_SIGNATURE); return Arrays.equals(header, FLAC_SIGNATURE);
} }
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
if (readPastStreamInfo) {
return;
}
FlacStreamInfo streamInfo = decodeStreamInfo(input);
readPastStreamInfo = true;
if (this.streamInfo == null) {
updateFlacStreamInfo(input, streamInfo);
}
}
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
outputSeekMap(input, streamInfo);
outputFormat(streamInfo);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
throws InterruptedException, IOException {
try {
FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
if (streamInfo == null) {
throw new IOException("Metadata decoding failed");
}
return streamInfo;
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
throw e;
}
}
private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
SeekMap seekMap =
hasSeekTable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: getSeekMapForNonSeekTableFlac(input, streamInfo);
extractorOutput.seekMap(seekMap);
}
private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
long inputLength = input.getLength();
if (inputLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
flacBinarySearchSeeker =
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
return flacBinarySearchSeeker.getSeekMap();
} else { // can't seek at all, because there's no SeekTable and the input length is unknown.
return new SeekMap.Unseekable(streamInfo.durationUs());
}
}
private void outputFormat(FlacStreamInfo streamInfo) {
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
streamInfo.bitRate(),
streamInfo.maxDecodedFrameSize(),
streamInfo.channels,
streamInfo.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
isId3MetadataDisabled ? null : id3Metadata);
trackOutput.format(mediaFormat);
}
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
throws InterruptedException, IOException {
int seekResult =
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
}
return seekResult;
}
private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
outputBuffer.setPosition(0);
trackOutput.sampleData(outputBuffer, size);
trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
}
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
private static final class FlacSeekMap implements SeekMap { private static final class FlacSeekMap implements SeekMap {
private final long durationUs; private final long durationUs;

View File

@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {
return context->parser->getDecodePosition(); return context->parser->getDecodePosition();
} }
DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) { DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastTimestamp(); return context->parser->getLastFrameTimestamp();
}
DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getLastFrameFirstSampleIndex();
}
DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->getNextFrameFirstSampleIndex();
} }
DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
return env->NewStringUTF(str); return env->NewStringUTF(str);
} }
DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext);
return context->parser->isDecoderAtEndOfStream();
}
DECODER_FUNC(void, flacFlush, jlong jContext) { DECODER_FUNC(void, flacFlush, jlong jContext) {
Context *context = reinterpret_cast<Context *>(jContext); Context *context = reinterpret_cast<Context *>(jContext);
context->parser->flush(); context->parser->flush();

View File

@ -44,10 +44,18 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
int64_t getLastTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
} }
int64_t getLastFrameFirstSampleIndex() const {
return mWriteHeader.number.sample_number;
}
int64_t getNextFrameFirstSampleIndex() const {
return mWriteHeader.number.sample_number + mWriteHeader.blocksize;
}
bool decodeMetadata(); bool decodeMetadata();
size_t readBuffer(void *output, size_t output_size); size_t readBuffer(void *output, size_t output_size);
@ -83,6 +91,11 @@ class FLACParser {
return FLAC__stream_decoder_get_resolved_state_string(mDecoder); return FLAC__stream_decoder_get_resolved_state_string(mDecoder);
} }
bool isDecoderAtEndOfStream() const {
return FLAC__stream_decoder_get_state(mDecoder) ==
FLAC__STREAM_DECODER_END_OF_STREAM;
}
private: private:
DataSource *mDataSource; DataSource *mDataSource;

View File

@ -649,6 +649,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override @Override
public void loadAd(String adUriString) { public void loadAd(String adUriString) {
try {
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
Log.w( Log.w(
TAG, TAG,
@ -660,7 +661,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex); Log.d(TAG, "loadAd in ad group " + adGroupIndex);
} }
try {
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) { if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");

View File

@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource {
// Check for a valid response code. // Check for a valid response code.
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
Map<String, List<String>> headers = request.headers().toMultimap(); Map<String, List<String>> headers = response.headers().toMultimap();
closeConnectionQuietly(); closeConnectionQuietly();
InvalidResponseCodeException exception = new InvalidResponseCodeException( InvalidResponseCodeException exception = new InvalidResponseCodeException(
responseCode, headers, dataSpec); responseCode, headers, dataSpec);

View File

@ -22,6 +22,13 @@ android {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
} }
// Workaround to prevent circular dependency on project :testutils. // Workaround to prevent circular dependency on project :testutils.
@ -42,19 +49,17 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
androidTestImplementation 'com.android.support.test:runner:' + testRunnerVersion
androidTestUtil 'com.android.support.test:orchestrator:' + testRunnerVersion
testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion

View File

@ -16,8 +16,8 @@
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
import android.app.Instrumentation;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
@ -28,48 +28,58 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.test.InstrumentationTestCase; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
/** /** Unit tests for {@link ContentDataSource}. */
* Unit tests for {@link ContentDataSource}. @RunWith(AndroidJUnit4.class)
*/ public final class ContentDataSourceTest {
public final class ContentDataSourceTest extends InstrumentationTestCase {
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
@Test
public void testRead() throws Exception { public void testRead() throws Exception {
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); assertData(0, C.LENGTH_UNSET, false);
} }
@Test
public void testReadPipeMode() throws Exception { public void testReadPipeMode() throws Exception {
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); assertData(0, C.LENGTH_UNSET, true);
} }
@Test
public void testReadFixedLength() throws Exception { public void testReadFixedLength() throws Exception {
assertData(getInstrumentation(), 0, 100, false); assertData(0, 100, false);
} }
@Test
public void testReadFromOffsetToEndOfInput() throws Exception { public void testReadFromOffsetToEndOfInput() throws Exception {
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); assertData(1, C.LENGTH_UNSET, false);
} }
@Test
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); assertData(1, C.LENGTH_UNSET, true);
} }
@Test
public void testReadFromOffsetFixedLength() throws Exception { public void testReadFromOffsetFixedLength() throws Exception {
assertData(getInstrumentation(), 1, 100, false); assertData(1, 100, false);
} }
@Test
public void testReadInvalidUri() throws Exception { public void testReadInvalidUri() throws Exception {
ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); ContentDataSource dataSource =
new ContentDataSource(InstrumentationRegistry.getTargetContext());
Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
DataSpec dataSpec = new DataSpec(contentUri); DataSpec dataSpec = new DataSpec(contentUri);
try { try {
@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
} }
} }
private static void assertData(Instrumentation instrumentation, int offset, int length, private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
boolean pipeMode) throws IOException {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); ContentDataSource dataSource =
new ContentDataSource(InstrumentationRegistry.getTargetContext());
try { try {
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH); byte[] completeData =
TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH);
byte[] expectedData = Arrays.copyOfRange(completeData, offset, byte[] expectedData = Arrays.copyOfRange(completeData, offset,
length == C.LENGTH_UNSET ? completeData.length : offset + length); length == C.LENGTH_UNSET ? completeData.length : offset + length);
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);

View File

@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -29,9 +30,14 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests {@link CachedContentIndex}. */ /** Tests {@link CachedContentIndex}. */
public class CachedContentIndexTest extends InstrumentationTestCase { @RunWith(AndroidJUnit4.class)
public class CachedContentIndexTest {
private final byte[] testIndexV1File = { private final byte[] testIndexV1File = {
0, 0, 0, 1, // version 0, 0, 0, 1, // version
@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
private CachedContentIndex index; private CachedContentIndex index;
private File cacheDir; private File cacheDir;
@Override @Before
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); cacheDir =
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @After
protected void tearDown() throws Exception { public void tearDown() {
Util.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
super.tearDown();
} }
@Test
public void testAddGetRemove() throws Exception { public void testAddGetRemove() throws Exception {
final String key1 = "key1"; final String key1 = "key1";
final String key2 = "key2"; final String key2 = "key2";
@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(cacheSpanFile.exists()).isTrue(); assertThat(cacheSpanFile.exists()).isTrue();
} }
@Test
public void testStoreAndLoad() throws Exception { public void testStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
} }
@Test
public void testLoadV1() throws Exception { public void testLoadV1() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV1File); fos.write(testIndexV1File);
@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
} }
@Test
public void testLoadV2() throws Exception { public void testLoadV2() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV2File); fos.write(testIndexV2File);
@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
} }
public void testAssignIdForKeyAndGetKeyForId() throws Exception { @Test
public void testAssignIdForKeyAndGetKeyForId() {
final String key1 = "key1"; final String key1 = "key1";
final String key2 = "key2"; final String key2 = "key2";
int id1 = index.assignIdForKey(key1); int id1 = index.assignIdForKey(key1);
@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.assignIdForKey(key2)).isEqualTo(id2); assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
} }
public void testGetNewId() throws Exception { @Test
public void testGetNewId() {
SparseArray<String> idToKey = new SparseArray<>(); SparseArray<String> idToKey = new SparseArray<>();
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
idToKey.put(10, ""); idToKey.put(10, "");
@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
} }
@Test
public void testEncryption() throws Exception { public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
} }
public void testRemoveEmptyNotLockedCachedContent() throws Exception { @Test
public void testRemoveEmptyNotLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
index.maybeRemove(cachedContent.key); index.maybeRemove(cachedContent.key);
@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNull(); assertThat(index.get(cachedContent.key)).isNull();
} }
@Test
public void testCantRemoveNotEmptyCachedContent() throws Exception { public void testCantRemoveNotEmptyCachedContent() throws Exception {
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
File cacheSpanFile = File cacheSpanFile =
@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertThat(index.get(cachedContent.key)).isNotNull(); assertThat(index.get(cachedContent.key)).isNotNull();
} }
public void testCantRemoveLockedCachedContent() throws Exception { @Test
public void testCantRemoveLockedCachedContent() {
CachedContent cachedContent = index.getOrAdd("key1"); CachedContent cachedContent = index.getOrAdd("key1");
cachedContent.setLocked(true); cachedContent.setLocked(true);

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.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import android.test.InstrumentationTestCase; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -26,11 +27,14 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** /** Unit tests for {@link SimpleCacheSpan}. */
* Unit tests for {@link SimpleCacheSpan}. @RunWith(AndroidJUnit4.class)
*/ public class SimpleCacheSpanTest {
public class SimpleCacheSpanTest extends InstrumentationTestCase {
private CachedContentIndex index; private CachedContentIndex index;
private File cacheDir; private File cacheDir;
@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
return SimpleCacheSpan.createCacheEntry(cacheFile, index); return SimpleCacheSpan.createCacheEntry(cacheFile, index);
} }
@Override @Before
protected void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); cacheDir =
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @After
protected void tearDown() throws Exception { public void tearDown() {
Util.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
super.tearDown();
} }
@Test
public void testCacheFile() throws Exception { public void testCacheFile() throws Exception {
assertCacheSpan("key1", 0, 0); assertCacheSpan("key1", 0, 0);
assertCacheSpan("key2", 1, 2); assertCacheSpan("key2", 1, 2);
@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
+ "A paragraph-separator character \u2029", 1, 2); + "A paragraph-separator character \u2029", 1, 2);
} }
@Test
public void testUpgradeFileName() throws Exception { public void testUpgradeFileName() throws Exception {
String key = "asd\u00aa"; String key = "asd\u00aa";
int id = index.assignIdForKey(key); int id = index.assignIdForKey(key);

View File

@ -46,9 +46,8 @@ public class DefaultLoadControl implements LoadControl {
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500;
/** /**
* The default duration of media that must be buffered for playback to resume after a rebuffer, * The default duration of media that must be buffered for playback to resume after a rebuffer, in
* in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.
* action.
*/ */
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;

View File

@ -185,10 +185,6 @@ public interface ExoPlayer extends Player {
*/ */
Looper getPlaybackLooper(); Looper getPlaybackLooper();
@Override
@Nullable
ExoPlaybackException getPlaybackError();
/** /**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to * Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true, true)}. * {@code prepare(mediaSource, true, true)}.

View File

@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.playWhenReady != playWhenReady) { if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
internalPlayer.setPlayWhenReady(playWhenReady); internalPlayer.setPlayWhenReady(playWhenReady);
PlaybackInfo playbackInfo = this.playbackInfo;
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
} }
@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
break; break;
case ExoPlayerImplInternal.MSG_ERROR: case ExoPlayerImplInternal.MSG_ERROR:
playbackError = (ExoPlaybackException) msg.obj; ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
this.playbackError = playbackError;
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
listener.onPlayerError(playbackError); listener.onPlayerError(playbackError);
} }
@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
boolean trackSelectorResultChanged = boolean trackSelectorResultChanged =
this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
playbackInfo = newPlaybackInfo; playbackInfo = newPlaybackInfo;
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {

View File

@ -854,6 +854,9 @@ import java.util.Collections;
} }
private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
if (message.isCanceled()) {
return;
}
try { try {
message.getTarget().handleMessage(message.getType(), message.getPayload()); message.getTarget().handleMessage(message.getType(), message.getPayload());
} finally { } finally {
@ -945,7 +948,7 @@ import java.util.Collections;
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
sendMessageToTarget(nextInfo.message); sendMessageToTarget(nextInfo.message);
if (nextInfo.message.getDeleteAfterDelivery()) { if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
pendingMessages.remove(nextPendingMessageIndex); pendingMessages.remove(nextPendingMessageIndex);
} else { } else {
nextPendingMessageIndex++; nextPendingMessageIndex++;

View File

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.8.0"; public static final String VERSION = "2.8.1";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2008000; public static final int VERSION_INT = 2008001;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
@ -87,7 +88,7 @@ public final class PlaybackParameters {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -63,6 +63,7 @@ public final class PlayerMessage {
private boolean isSent; private boolean isSent;
private boolean isDelivered; private boolean isDelivered;
private boolean isProcessed; private boolean isProcessed;
private boolean isCanceled;
/** /**
* Creates a new message. * Creates a new message.
@ -242,6 +243,24 @@ public final class PlayerMessage {
return this; 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 * Blocks until after the message has been delivered or the player is no longer able to deliver
* the message. * the message.

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
/** /**
* The configuration of a {@link Renderer}. * The configuration of a {@link Renderer}.
*/ */
@ -41,7 +43,7 @@ public final class RendererConfiguration {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
@ -71,7 +72,7 @@ public final class SeekParameters {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
private float audioVolume; private float audioVolume;
private MediaSource mediaSource; private MediaSource mediaSource;
private List<Cue> currentCues;
/** /**
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
@ -177,6 +178,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioAttributes = AudioAttributes.DEFAULT; audioAttributes = AudioAttributes.DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
currentCues = Collections.emptyList();
// Build the player and associated objects. // Build the player and associated objects.
player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
@ -502,6 +504,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override @Override
public void addTextOutput(TextOutput listener) { public void addTextOutput(TextOutput listener) {
if (!currentCues.isEmpty()) {
listener.onCues(currentCues);
}
textOutputs.add(listener); textOutputs.add(listener);
} }
@ -775,6 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
mediaSource = null; mediaSource = null;
analyticsCollector.resetForNewMediaSource(); analyticsCollector.resetForNewMediaSource();
} }
currentCues = Collections.emptyList();
} }
@Override @Override
@ -790,6 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
if (mediaSource != null) { if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector); mediaSource.removeEventListener(analyticsCollector);
} }
currentCues = Collections.emptyList();
} }
@Override @Override
@ -1095,6 +1102,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
@Override @Override
public void onCues(List<Cue> cues) { public void onCues(List<Cue> cues) {
currentCues = cues;
for (TextOutput textOutput : textOutputs) { for (TextOutput textOutput : textOutputs) {
textOutput.onCues(cues); textOutput.onCues(cues);
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
/** /**
@ -119,7 +120,7 @@ public final class AudioAttributes {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -22,6 +22,7 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
/** /**
@ -96,7 +97,7 @@ public final class AudioCapabilities {
} }
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (this == other) { if (this == other) {
return true; return true;
} }

View File

@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SchemeData)) { if (!(obj instanceof SchemeData)) {
return false; return false;
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -92,7 +93,7 @@ public interface SeekMap {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import android.support.annotation.Nullable;
/** Defines a seek point in a media stream. */ /** Defines a seek point in a media stream. */
public final class SeekPoint { public final class SeekPoint {
@ -42,7 +44,7 @@ public final class SeekPoint {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
@ -69,7 +70,7 @@ public interface TrackOutput {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -189,10 +189,12 @@ import java.util.List;
} }
} }
// True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. // Fixed sample size raw audio may need to be rechunked.
boolean isRechunkable = sampleSizeBox.isFixedSampleSize() boolean isFixedSampleSizeRawAudio =
sampleSizeBox.isFixedSampleSize()
&& MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
&& remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 && remainingTimestampDeltaChanges == 0
&& remainingTimestampOffsetChanges == 0
&& remainingSynchronizationSamples == 0; && remainingSynchronizationSamples == 0;
long[] offsets; long[] offsets;
@ -203,7 +205,7 @@ import java.util.List;
long timestampTimeUnits = 0; long timestampTimeUnits = 0;
long duration; long duration;
if (!isRechunkable) { if (!isFixedSampleSizeRawAudio) {
offsets = new long[sampleCount]; offsets = new long[sampleCount];
sizes = new int[sampleCount]; sizes = new int[sampleCount];
timestamps = new long[sampleCount]; timestamps = new long[sampleCount];
@ -296,7 +298,8 @@ import java.util.List;
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
} }
int fixedSampleSize = sampleSizeBox.readNextSampleSize(); int fixedSampleSize =
Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
offsets = rechunkedResults.offsets; offsets = rechunkedResults.offsets;
@ -1224,7 +1227,7 @@ import java.util.List;
stsc.setPosition(Atom.FULL_HEADER_SIZE); stsc.setPosition(Atom.FULL_HEADER_SIZE);
remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
index = C.INDEX_UNSET; index = -1;
} }
public boolean moveNext() { public boolean moveNext() {

View File

@ -482,13 +482,13 @@ public final class MediaCodecUtil {
return null; return null;
} }
Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger); int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);
if (profile == null) { if (profile == -1) {
Log.w(TAG, "Unknown AVC profile: " + profileInteger); Log.w(TAG, "Unknown AVC profile: " + profileInteger);
return null; return null;
} }
Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger); int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);
if (level == null) { if (level == -1) {
Log.w(TAG, "Unknown AVC level: " + levelInteger); Log.w(TAG, "Unknown AVC level: " + levelInteger);
return null; return null;
} }
@ -639,7 +639,7 @@ public final class MediaCodecUtil {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -76,7 +77,7 @@ public final class Metadata implements Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
/** /**
@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -140,7 +140,7 @@ public abstract class DownloadAction {
DownloaderConstructorHelper downloaderConstructorHelper); DownloaderConstructorHelper downloaderConstructorHelper);
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (o == null || getClass() != o.getClass()) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }

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.DataSource;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -250,7 +251,6 @@ public final class DownloadManager {
Assertions.checkState(!released); Assertions.checkState(!released);
Task task = addTaskForAction(action); Task task = addTaskForAction(action);
if (initialized) { if (initialized) {
notifyListenersTaskStateChange(task);
saveActions(); saveActions();
maybeStartTasks(); maybeStartTasks();
if (task.currentState == STATE_QUEUED) { if (task.currentState == STATE_QUEUED) {
@ -413,7 +413,6 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
logd("Task state is changed", task);
boolean stopped = !task.isActive(); boolean stopped = !task.isActive();
if (stopped) { if (stopped) {
activeDownloadTasks.remove(task); activeDownloadTasks.remove(task);
@ -430,6 +429,7 @@ public final class DownloadManager {
} }
private void notifyListenersTaskStateChange(Task task) { private void notifyListenersTaskStateChange(Task task) {
logd("Task state is changed", task);
TaskState taskState = task.getDownloadState(); TaskState taskState = task.getDownloadState();
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onTaskStateChanged(this, taskState); listener.onTaskStateChanged(this, taskState);
@ -468,18 +468,16 @@ public final class DownloadManager {
listener.onInitialized(DownloadManager.this); listener.onInitialized(DownloadManager.this);
} }
if (!pendingTasks.isEmpty()) { if (!pendingTasks.isEmpty()) {
for (int i = 0; i < pendingTasks.size(); i++) { tasks.addAll(pendingTasks);
tasks.add(pendingTasks.get(i));
}
saveActions(); saveActions();
} }
maybeStartTasks(); maybeStartTasks();
for (int i = 0; i < pendingTasks.size(); i++) { for (int i = 0; i < tasks.size(); i++) {
Task pendingTask = pendingTasks.get(i); Task task = tasks.get(i);
if (pendingTask.currentState == STATE_QUEUED) { if (task.currentState == STATE_QUEUED) {
// Task did not change out of its initial state, and so its initial state // Task did not change out of its initial state, and so its initial state
// won't have been reported to listeners. Do so now. // won't have been reported to listeners. Do so now.
notifyListenersTaskStateChange(pendingTask); notifyListenersTaskStateChange(task);
} }
} }
} }
@ -699,9 +697,19 @@ public final class DownloadManager {
+ ' ' + ' '
+ (action.isRemoveAction ? "remove" : "download") + (action.isRemoveAction ? "remove" : "download")
+ ' ' + ' '
+ toString(action.data)
+ ' '
+ getStateString(); + getStateString();
} }
private static String toString(byte[] data) {
if (data.length > 100) {
return "<data is too long>";
} else {
return '\'' + Util.fromUtf8Bytes(data) + '\'';
}
}
private String getStateString() { private String getStateString() {
switch (currentState) { switch (currentState) {
case STATE_QUEUED_CANCELING: case STATE_QUEUED_CANCELING:

View File

@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }

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; protected abstract void writeKey(DataOutputStream output, K key) throws IOException;
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }

View File

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

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.util.Arrays; import java.util.Arrays;
@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -78,6 +78,25 @@ public class Cue {
*/ */
public static final int LINE_TYPE_NUMBER = 1; 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 * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated
* with styling spans. * with styling spans.
@ -106,40 +125,39 @@ public class Cue {
/** /**
* The type of the {@link #line} value. * 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. * viewport.
* <p> *
* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each * <p>{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of
* line is taken to be the size of the first line of the cue. When {@link #line} is greater than * each line is taken to be the size of the first line of the cue. When {@link #line} is greater
* or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset
* the start edge. When {@link #line} is negative lines count from the end of the viewport, with * from the start edge. When {@link #line} is negative lines count from the end of the viewport,
* -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the
* of the first line of the cue, and the start and end of the viewport are the top and bottom * height of the first line of the cue, and the start and end of the viewport are the top and
* respectively. * bottom respectively.
* <p> *
* Note that it's particularly important to consider the effect of {@link #lineAnchor} when using * <p>Note that it's particularly important to consider the effect of {@link #lineAnchor} when
* {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)}
* (potentially multi-line) cue at the very top of the viewport. * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 &&
* {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of
* at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 &&
* and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line
* the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible
* the last line is visible at the top of the viewport. * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a
* {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first * cue so that only its first line is visible at the bottom of the viewport.
* 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}, * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * #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} * <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of
* respectively. * 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 * 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; public final float position;
/** /**
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * #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} * <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of
* respectively. * 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 * 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; 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. * Creates an image cue.
* *
@ -194,17 +224,36 @@ public class Cue {
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * {@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 * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a
* fraction of the viewport height. * fraction of the viewport height.
* @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * #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 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 * @param height The height of the cue as a fraction of the viewport height, or {@link
* {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified
* specified {@code width}. * {@code width}.
*/ */
public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, public Cue(
float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { Bitmap bitmap,
this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, float horizontalPosition,
horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); @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}. * @param text See {@link #text}.
*/ */
public Cue(CharSequence 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 positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}. * @param size See {@link #size}.
*/ */
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, public Cue(
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { CharSequence text,
this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, Alignment textAlignment,
Color.BLACK); 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 windowColorSet See {@link #windowColorSet}.
* @param windowColor See {@link #windowColor}. * @param windowColor See {@link #windowColor}.
*/ */
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, public Cue(
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, CharSequence text,
boolean windowColorSet, int windowColor) { Alignment textAlignment,
this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, float line,
DIMEN_UNSET, windowColorSet, windowColor); @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, private Cue(
@LineType int lineType, @AnchorType int lineAnchor, float position, CharSequence text,
@AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, 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) { int windowColor) {
this.text = text; this.text = text;
this.textAlignment = textAlignment; this.textAlignment = textAlignment;
@ -272,6 +419,8 @@ public class Cue {
this.bitmapHeight = bitmapHeight; this.bitmapHeight = bitmapHeight;
this.windowColorSet = windowColorSet; this.windowColorSet = windowColorSet;
this.windowColor = windowColor; 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 * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
* supported by this decoder are: * supported by this decoder are:
*
* <ul> * <ul>
* <li>content * <li>content
* <li>core * <li>core
@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
* <li>time-clock * <li>time-clock
* <li>time-offset-with-frames * <li>time-offset-with-frames
* <li>time-offset-with-ticks * <li>time-offset-with-ticks
* <li>cell-resolution
* </ul> * </ul>
*
* @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a> * @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
*/ */
public final class TtmlDecoder extends SimpleSubtitleDecoder { public final class TtmlDecoder extends SimpleSubtitleDecoder {
@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES = private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
private static final int DEFAULT_FRAME_RATE = 30; private static final int DEFAULT_FRAME_RATE = 30;
private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE = private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1); new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
private static final CellResolution DEFAULT_CELL_RESOLUTION =
new CellResolution(/* columns= */ 32, /* rows= */ 15);
private final XmlPullParserFactory xmlParserFactory; private final XmlPullParserFactory xmlParserFactory;
@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int unsupportedNodeDepth = 0; int unsupportedNodeDepth = 0;
int eventType = xmlParser.getEventType(); int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
while (eventType != XmlPullParser.END_DOCUMENT) { while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peekLast(); TtmlNode parent = nodeStack.peekLast();
if (unsupportedNodeDepth == 0) { if (unsupportedNodeDepth == 0) {
@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
if (TtmlNode.TAG_TT.equals(name)) { if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser); frameAndTickRate = parseFrameAndTickRates(xmlParser);
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
} }
if (!isSupportedTag(name)) { if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++; unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) { } else if (TtmlNode.TAG_HEAD.equals(name)) {
parseHeader(xmlParser, globalStyles, regionMap); parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
} else { } else {
try { try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
} }
private Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser, private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions) throws SubtitleDecoderException {
String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
if (cellResolution == null) {
return defaultValue;
}
Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution);
if (!cellResolutionMatcher.matches()) {
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
return defaultValue;
}
try {
int columns = Integer.parseInt(cellResolutionMatcher.group(1));
int rows = Integer.parseInt(cellResolutionMatcher.group(2));
if (columns == 0 || rows == 0) {
throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows);
}
return new CellResolution(columns, rows);
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
return defaultValue;
}
}
private Map<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> globalRegions,
CellResolution cellResolution)
throws IOException, XmlPullParserException { throws IOException, XmlPullParserException {
do { do {
xmlParser.next(); xmlParser.next();
@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style); globalStyles.put(style.getId(), style);
} }
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
if (ttmlRegion != null) { if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion); globalRegions.put(ttmlRegion.id, ttmlRegion);
} }
@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
/** /**
* Parses a region declaration. * Parses a region declaration.
* <p> *
* If the region defines an origin and extent, it is required that they're defined as percentages * <p>If the region defines an origin and extent, it is required that they're defined as
* of the viewport. Region declarations that define origin and extent in other formats are * percentages of the viewport. Region declarations that define origin and extent in other formats
* unsupported, and null is returned. * are unsupported, and null is returned.
*/ */
private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) { if (regionId == null) {
return null; return null;
@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} }
} }
return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width); float regionTextHeight = 1.0f / cellResolution.rows;
return new TtmlRegion(
regionId,
position,
line,
/* lineType= */ Cue.LINE_TYPE_FRACTION,
lineAnchor,
width,
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
/* textSize= */ regionTextHeight);
} }
private String[] parseStyleIds(String parentStyleIds) { private String[] parseStyleIds(String parentStyleIds) {
@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.tickRate = tickRate; this.tickRate = tickRate;
} }
} }
/** Represents the cell resolution for a TTML file. */
private static final class CellResolution {
final int columns;
final int rows;
CellResolution(int columns, int rows) {
this.columns = columns;
this.rows = rows;
}
}
} }

View File

@ -175,35 +175,51 @@ import java.util.TreeSet;
Map<String, TtmlRegion> regionMap) { Map<String, TtmlRegion> regionMap) {
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>(); TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionOutputs); traverseForText(timeUs, false, regionId, regionOutputs);
traverseForStyle(globalStyles, regionOutputs); traverseForStyle(timeUs, globalStyles, regionOutputs);
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, cues.add(
region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); new Cue(
cleanUpText(entry.getValue()),
/* textAlignment= */ null,
region.line,
region.lineType,
region.lineAnchor,
region.position,
/* positionAnchor= */ Cue.TYPE_UNSET,
region.width,
region.textSizeType,
region.textSize));
} }
return cues; return cues;
} }
private void traverseForText(long timeUs, boolean descendsPNode, private void traverseForText(
String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) { long timeUs,
boolean descendsPNode,
String inheritedRegion,
Map<String, SpannableStringBuilder> regionOutputs) {
nodeStartsByRegion.clear(); nodeStartsByRegion.clear();
nodeEndsByRegion.clear(); nodeEndsByRegion.clear();
String resolvedRegionId = regionId; if (TAG_METADATA.equals(tag)) {
if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) { // Ignore metadata tag.
resolvedRegionId = inheritedRegion; return;
} }
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
if (isTextNode && descendsPNode) { if (isTextNode && descendsPNode) {
getRegionOutput(resolvedRegionId, regionOutputs).append(text); getRegionOutput(resolvedRegionId, regionOutputs).append(text);
} else if (TAG_BR.equals(tag) && descendsPNode) { } else if (TAG_BR.equals(tag) && descendsPNode) {
getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
} else if (TAG_METADATA.equals(tag)) {
// Do nothing.
} else if (isActive(timeUs)) { } else if (isActive(timeUs)) {
boolean isPNode = TAG_P.equals(tag); // This is a container node, which can contain zero or more children.
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
} }
boolean isPNode = TAG_P.equals(tag);
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId, getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
regionOutputs); regionOutputs);
@ -211,41 +227,52 @@ import java.util.TreeSet;
if (isPNode) { if (isPNode) {
TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
} }
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) { for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
} }
} }
} }
private static SpannableStringBuilder getRegionOutput(String resolvedRegionId, private static SpannableStringBuilder getRegionOutput(
Map<String, SpannableStringBuilder> regionOutputs) { String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {
if (!regionOutputs.containsKey(resolvedRegionId)) { if (!regionOutputs.containsKey(resolvedRegionId)) {
regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
} }
return regionOutputs.get(resolvedRegionId); return regionOutputs.get(resolvedRegionId);
} }
private void traverseForStyle(Map<String, TtmlStyle> globalStyles, private void traverseForStyle(
long timeUs,
Map<String, TtmlStyle> globalStyles,
Map<String, SpannableStringBuilder> regionOutputs) { Map<String, SpannableStringBuilder> regionOutputs) {
if (!isActive(timeUs)) {
return;
}
for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) { for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
String regionId = entry.getKey(); String regionId = entry.getKey();
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue()); int end = entry.getValue();
for (int i = 0; i < getChildCount(); ++i) { if (start != end) {
getChild(i).traverseForStyle(globalStyles, regionOutputs); SpannableStringBuilder regionOutput = regionOutputs.get(regionId);
applyStyleToOutput(globalStyles, regionOutput, start, end);
} }
} }
for (int i = 0; i < getChildCount(); ++i) {
getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
}
} }
private void applyStyleToOutput(Map<String, TtmlStyle> globalStyles, private void applyStyleToOutput(
SpannableStringBuilder regionOutput, int start, int end) { Map<String, TtmlStyle> globalStyles,
if (start != end) { SpannableStringBuilder regionOutput,
int start,
int end) {
TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
if (resolvedStyle != null) { if (resolvedStyle != null) {
TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
} }
} }
}
private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) { private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
// Having joined the text elements, we need to do some final cleanup on the result. // Having joined the text elements, we need to do some final cleanup on the result.

View File

@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue;
public final String id; public final String id;
public final float position; public final float position;
public final float line; public final float line;
@Cue.LineType public final int lineType; public final @Cue.LineType int lineType;
@Cue.AnchorType public final int lineAnchor; public final @Cue.AnchorType int lineAnchor;
public final float width; public final float width;
public final @Cue.TextSizeType int textSizeType;
public final float textSize;
public TtmlRegion(String id) { public TtmlRegion(String id) {
this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); this(
id,
/* position= */ Cue.DIMEN_UNSET,
/* line= */ Cue.DIMEN_UNSET,
/* lineType= */ Cue.TYPE_UNSET,
/* lineAnchor= */ Cue.TYPE_UNSET,
/* width= */ Cue.DIMEN_UNSET,
/* textSizeType= */ Cue.TYPE_UNSET,
/* textSize= */ Cue.DIMEN_UNSET);
} }
public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, public TtmlRegion(
@Cue.AnchorType int lineAnchor, float width) { String id,
float position,
float line,
@Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor,
float width,
int textSizeType,
float textSize) {
this.id = id; this.id = id;
this.position = position; this.position = position;
this.line = line; this.line = line;
this.lineType = lineType; this.lineType = lineType;
this.lineAnchor = lineAnchor; this.lineAnchor = lineAnchor;
this.width = width; this.width = width;
this.textSizeType = textSizeType;
this.textSize = textSize;
} }
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.trackselection; package com.google.android.exoplayer2.trackselection;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -20,6 +20,7 @@ import android.graphics.Point;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }
@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.trackselection; package com.google.android.exoplayer2.trackselection;
import android.support.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
/** An array of {@link TrackSelection}s. */ /** An array of {@link TrackSelection}s. */
@ -64,7 +65,7 @@ public final class TrackSelectionArray {
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.io.IOException; import java.io.IOException;
@ -79,7 +80,7 @@ public interface DataSource {
* *
* @return The {@link Uri} from which data is being read, or null if the source is not open. * @return The {@link Uri} from which data is being read, or null if the source is not open.
*/ */
Uri getUri(); @Nullable Uri getUri();
/** /**
* Closes the source. * Closes the source.

View File

@ -61,7 +61,7 @@ public final class DataSpec {
/** /**
* Body for a POST request, null otherwise. * Body for a POST request, null otherwise.
*/ */
public final byte[] postBody; public final @Nullable byte[] postBody;
/** /**
* The absolute position of the data in the full stream. * The absolute position of the data in the full stream.
*/ */
@ -81,12 +81,12 @@ public final class DataSpec {
* A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
* {@link DataSpec} is not intended to be used in conjunction with a cache. * {@link DataSpec} is not intended to be used in conjunction with a cache.
*/ */
@Nullable public final String key; public final @Nullable String key;
/** /**
* Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
* {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
*/ */
@Flags public final int flags; public final @Flags int flags;
/** /**
* Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
@ -128,7 +128,8 @@ public final class DataSpec {
* @param key {@link #key}. * @param key {@link #key}.
* @param flags {@link #flags}. * @param flags {@link #flags}.
*/ */
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) { public DataSpec(
Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) {
this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
} }
@ -143,7 +144,12 @@ public final class DataSpec {
* @param key {@link #key}. * @param key {@link #key}.
* @param flags {@link #flags}. * @param flags {@link #flags}.
*/ */
public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, public DataSpec(
Uri uri,
long absoluteStreamPosition,
long position,
long length,
@Nullable String key,
@Flags int flags) { @Flags int flags) {
this(uri, null, absoluteStreamPosition, position, length, key, flags); this(uri, null, absoluteStreamPosition, position, length, key, flags);
} }
@ -162,7 +168,7 @@ public final class DataSpec {
*/ */
public DataSpec( public DataSpec(
Uri uri, Uri uri,
byte[] postBody, @Nullable byte[] postBody,
long absoluteStreamPosition, long absoluteStreamPosition,
long position, long position,
long length, long length,
@ -222,4 +228,13 @@ public final class DataSpec {
} }
} }
/**
* 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; package com.google.android.exoplayer2.upstream;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException; import java.io.IOException;
@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource {
} }
@Override @Override
public Uri getUri() { public @Nullable Uri getUri() {
return upstream.getUri(); return upstream.getUri();
} }

View File

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

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.DataInputStream; import java.io.DataInputStream;
@ -236,7 +237,7 @@ import java.util.TreeSet;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -26,7 +25,6 @@ import java.io.BufferedInputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1; private static final int FLAG_ENCRYPTED_INDEX = 1;
private static final String TAG = "CachedContentIndex";
private final HashMap<String, CachedContent> keyToContent; private final HashMap<String, CachedContent> keyToContent;
private final SparseArray<String> idToKey; private final SparseArray<String> idToKey;
private final AtomicFile atomicFile; private final AtomicFile atomicFile;
@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec;
add(cachedContent); add(cachedContent);
hashCode += cachedContent.headerHashCode(version); hashCode += cachedContent.headerHashCode(version);
} }
if (input.readInt() != hashCode) { int fileHashCode = input.readInt();
boolean isEOF = input.read() == -1;
if (fileHashCode != hashCode || !isEOF) {
return false; return false;
} }
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error reading cache content index file.", e);
return false; return false;
} finally { } finally {
if (input != null) { if (input != null) {

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {
return true; 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; package com.google.android.exoplayer2.util;
import com.google.android.exoplayer2.C;
/** /**
* Holder for FLAC stream info. * Holder for FLAC stream info.
*/ */
@ -52,8 +54,29 @@ public final class FlacStreamInfo {
// Remaining 16 bytes is md5 value // Remaining 16 bytes is md5 value
} }
public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, /**
int sampleRate, int channels, int bitsPerSample, long totalSamples) { * Constructs a FlacStreamInfo given the parameters.
*
* @param minBlockSize Minimum block size of the FLAC stream.
* @param maxBlockSize Maximum block size of the FLAC stream.
* @param minFrameSize Minimum frame size of the FLAC stream.
* @param maxFrameSize Maximum frame size of the FLAC stream.
* @param sampleRate Sample rate of the FLAC stream.
* @param channels Number of channels of the FLAC stream.
* @param bitsPerSample Number of bits per sample of the FLAC stream.
* @param totalSamples Total samples of the FLAC stream.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/
public FlacStreamInfo(
int minBlockSize,
int maxBlockSize,
int minFrameSize,
int maxFrameSize,
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples) {
this.minBlockSize = minBlockSize; this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize; this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize; this.minFrameSize = minFrameSize;
@ -64,16 +87,43 @@ public final class FlacStreamInfo {
this.totalSamples = totalSamples; this.totalSamples = totalSamples;
} }
/** Returns the maximum size for a decoded frame from the FLAC stream. */
public int maxDecodedFrameSize() { public int maxDecodedFrameSize() {
return maxBlockSize * channels * (bitsPerSample / 8); return maxBlockSize * channels * (bitsPerSample / 8);
} }
/** Returns the bit-rate of the FLAC stream. */
public int bitRate() { public int bitRate() {
return bitsPerSample * sampleRate; return bitsPerSample * sampleRate;
} }
/** Returns the duration of the FLAC stream in microseconds. */
public long durationUs() { public long durationUs() {
return (totalSamples * 1000000L) / sampleRate; return (totalSamples * 1000000L) / sampleRate;
} }
/**
* Returns the sample index for the sample at given position.
*
* @param timeUs Time position in microseconds in the FLAC stream.
* @return The sample index for the sample at given position.
*/
public long getSampleIndex(long timeUs) {
long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
return Util.constrainValue(sampleIndex, 0, totalSamples - 1);
}
/** Returns the approximate number of bytes per frame for the current FLAC stream. */
public long getApproxBytesPerFrame() {
long approxBytesPerFrame;
if (maxFrameSize > 0) {
approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;
} else {
// Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the
// default value for FLAC block-size, which is 4096.
long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096;
approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64;
}
return approxBytesPerFrame;
}
} }

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. * 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.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
/** /**
@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable {
// Parcelable implementation. // Parcelable implementation.
@Override @Override
public boolean equals(Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
} }

View File

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

View File

@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4006, // https://github.com/google/ExoPlayer/issues/4006,
// https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104. // https://github.com/google/ExoPlayer/issues/4104.
// https://github.com/google/ExoPlayer/issues/4134.
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|| "mido".equals(Util.DEVICE) // Redmi Note 4 || "mido".equals(Util.DEVICE) // Redmi Note 4
@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|| "F3311".equals(Util.DEVICE) // Sony Xperia E5 || "F3311".equals(Util.DEVICE) // Sony Xperia E5
|| "M5c".equals(Util.DEVICE) // Meizu M5C || "M5c".equals(Util.DEVICE) // Meizu M5C
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
|| "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note
|| "woods_f".equals(Util.MODEL)) // Moto E (4)
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II

View File

@ -51,6 +51,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@ -1812,6 +1813,88 @@ public final class ExoPlayerTest {
assertThat(target3.windowIndex).isEqualTo(2); 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 @Test
public void testSetAndSwitchSurface() throws Exception { public void testSetAndSwitchSurface() throws Exception {
final List<Integer> rendererMessages = new ArrayList<>(); final List<Integer> rendererMessages = new ArrayList<>();
@ -1934,8 +2017,10 @@ public final class ExoPlayerTest {
@Override @Override
public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { public void handleMessage(SimpleExoPlayer player, int messageType, Object message) {
if (player != null) {
windowIndex = player.getCurrentWindowIndex(); windowIndex = player.getCurrentWindowIndex();
positionMs = player.getCurrentPosition(); positionMs = player.getCurrentPosition();
}
messageCount++; messageCount++;
} }
} }

View File

@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest {
} }
@Override @Override
public boolean equals(Object other) { public boolean equals(@Nullable Object other) {
if (!(other instanceof EventWindowAndPeriodId)) { if (!(other instanceof EventWindowAndPeriodId)) {
return false; return false;
} }

View File

@ -15,9 +15,11 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import static com.google.android.exoplayer2.util.UriUtil.removeQueryParameter;
import static com.google.android.exoplayer2.util.UriUtil.resolve; import static com.google.android.exoplayer2.util.UriUtil.resolve;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
@ -104,4 +106,36 @@ public final class UriUtilTest {
assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); assertThat(resolve("a:b", "../c")).isEqualTo("a:c");
} }
@Test
public void removeOnlyQueryParameter() {
Uri uri = Uri.parse("http://uri?query=value");
assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri");
}
@Test
public void removeFirstQueryParameter() {
Uri uri = Uri.parse("http://uri?query=value&second=value2");
assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?second=value2");
}
@Test
public void removeMiddleQueryParameter() {
Uri uri = Uri.parse("http://uri?first=value1&query=value&last=value2");
assertThat(removeQueryParameter(uri, "query").toString())
.isEqualTo("http://uri?first=value1&last=value2");
}
@Test
public void removeLastQueryParameter() {
Uri uri = Uri.parse("http://uri?first=value1&query=value");
assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?first=value1");
}
@Test
public void removeNonExistentQueryParameter() {
Uri uri = Uri.parse("http://uri");
assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri");
uri = Uri.parse("http://uri?query=value");
assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri?query=value");
}
} }

View File

@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper {
manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri);
} }
/** Returns the DASH manifest. Must not be called until after preparation completes. */
public DashManifest getManifest() {
Assertions.checkNotNull(manifest);
return manifest;
}
@Override @Override
public int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(manifest); Assertions.checkNotNull(manifest);

View File

@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -198,24 +198,24 @@ import java.util.List;
/** /**
* Returns the next chunk to load. * Returns the next chunk to load.
* <p> *
* If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has * <p>If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream
* been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available
* the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to * but the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
* contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * contain the {@link HlsUrl} that refers to the playlist that needs refreshing.
* *
* @param previous The most recently loaded media chunk. * @param previous The most recently loaded media chunk.
* @param playbackPositionUs The current playback position in microseconds. If playback of the * @param playbackPositionUs The current playback position relative to the period start in
* period to which this chunk source belongs has not yet started, the value will be the * microseconds. If playback of the period to which this chunk source belongs has not yet
* starting position in the period minus the duration of any media in previous periods still * started, the value will be the starting position in the period minus the duration of any
* to be played. * media in previous periods still to be played.
* @param loadPositionUs The current load position in microseconds. If {@code previous} is null, * @param loadPositionUs The current load position relative to the period start in microseconds.
* this is the starting position from which chunks should be provided. Else it's equal to * If {@code previous} is null, this is the starting position from which chunks should be
* {@code previous.endTimeUs}. * provided. Else it's equal to {@code previous.endTimeUs}.
* @param out A holder to populate. * @param out A holder to populate.
*/ */
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, public void getNextChunk(
HlsChunkHolder out) { HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) {
int oldVariantIndex = previous == null ? C.INDEX_UNSET int oldVariantIndex = previous == null ? C.INDEX_UNSET
: trackGroup.indexOf(previous.trackFormat); : trackGroup.indexOf(previous.trackFormat);
long bufferedDurationUs = loadPositionUs - playbackPositionUs; long bufferedDurationUs = loadPositionUs - playbackPositionUs;
@ -261,12 +261,13 @@ import java.util.List;
// If the playlist is too old to contain the chunk, we need to refresh it. // If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
} else { } else {
// The playlist start time is subtracted from the target position because the segment start long positionOfPlaylistInPeriodUs =
// times are relative to the start of the playlist, but the target position is not. mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs;
chunkMediaSequence = chunkMediaSequence =
Util.binarySearchFloor( Util.binarySearchFloor(
mediaPlaylist.segments, mediaPlaylist.segments,
/* value= */ targetPositionUs - mediaPlaylist.startTimeUs, /* value= */ targetPositionInPlaylistUs,
/* inclusive= */ true, /* inclusive= */ true,
/* stayInBounds= */ !playlistTracker.isLive() || previous == null) /* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ mediaPlaylist.mediaSequence; + mediaPlaylist.mediaSequence;
@ -330,9 +331,9 @@ import java.util.List;
} }
// Compute start time of the next chunk. // Compute start time of the next chunk.
long offsetFromInitialStartTimeUs = long positionOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs; long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
int discontinuitySequence = mediaPlaylist.discontinuitySequence int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ segment.relativeDiscontinuitySequence; + segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
@ -352,8 +353,8 @@ import java.util.List;
muxedCaptionFormats, muxedCaptionFormats,
trackSelection.getSelectionReason(), trackSelection.getSelectionReason(),
trackSelection.getSelectionData(), trackSelection.getSelectionData(),
startTimeUs, segmentStartTimeInPeriodUs,
startTimeUs + segment.durationUs, segmentStartTimeInPeriodUs + segment.durationUs,
chunkMediaSequence, chunkMediaSequence,
discontinuitySequence, discontinuitySequence,
segment.hasGapTag, segment.hasGapTag,

View File

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
/** /**
@ -36,6 +37,11 @@ import java.io.IOException;
sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
} }
public void bindSampleQueue() {
Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
}
public void unbindSampleQueue() { public void unbindSampleQueue() {
if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
@ -48,12 +54,11 @@ import java.io.IOException;
@Override @Override
public boolean isReady() { public boolean isReady() {
return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
|| (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex)); || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
} }
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
maybeMapToSampleQueue();
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {
throw new SampleQueueMappingException( throw new SampleQueueMappingException(
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
@ -63,22 +68,21 @@ import java.io.IOException;
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
return maybeMapToSampleQueue() return hasValidSampleQueueIndex()
? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat)
: C.RESULT_NOTHING_READ; : C.RESULT_NOTHING_READ;
} }
@Override @Override
public int skipData(long positionUs) { public int skipData(long positionUs) {
return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0; return hasValidSampleQueueIndex()
? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)
: 0;
} }
// Internal methods. // Internal methods.
private boolean maybeMapToSampleQueue() { private boolean hasValidSampleQueueIndex() {
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
}
return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;

View File

@ -102,6 +102,7 @@ import java.util.Arrays;
private final Runnable maybeFinishPrepareRunnable; private final Runnable maybeFinishPrepareRunnable;
private final Runnable onTracksEndedRunnable; private final Runnable onTracksEndedRunnable;
private final Handler handler; private final Handler handler;
private final ArrayList<HlsSampleStream> hlsSampleStreams;
private SampleQueue[] sampleQueues; private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds; private int[] sampleQueueTrackIds;
@ -166,6 +167,7 @@ import java.util.Arrays;
sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0]; sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>(); mediaChunks = new ArrayList<>();
hlsSampleStreams = new ArrayList<>();
maybeFinishPrepareRunnable = maybeFinishPrepareRunnable =
new Runnable() { new Runnable() {
@Override @Override
@ -219,9 +221,6 @@ import java.util.Arrays;
} }
public int bindSampleQueueToSampleStream(int trackGroupIndex) { public int bindSampleQueueToSampleStream(int trackGroupIndex) {
if (trackGroupToSampleQueueIndex == null) {
return SAMPLE_QUEUE_INDEX_PENDING;
}
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
if (sampleQueueIndex == C.INDEX_UNSET) { if (sampleQueueIndex == C.INDEX_UNSET) {
return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET
@ -295,6 +294,9 @@ import java.util.Arrays;
} }
streams[i] = new HlsSampleStream(this, trackGroupIndex); streams[i] = new HlsSampleStream(this, trackGroupIndex);
streamResetFlags[i] = true; streamResetFlags[i] = true;
if (trackGroupToSampleQueueIndex != null) {
((HlsSampleStream) streams[i]).bindSampleQueue();
}
// If there's still a chance of avoiding a seek, try and seek within the sample queue. // If there's still a chance of avoiding a seek, try and seek within the sample queue.
if (sampleQueuesBuilt && !seekRequired) { if (sampleQueuesBuilt && !seekRequired) {
SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]];
@ -360,6 +362,7 @@ import java.util.Arrays;
} }
} }
updateSampleStreams(streams);
seenFirstTrackSelection = true; seenFirstTrackSelection = true;
return seekRequired; return seekRequired;
} }
@ -411,6 +414,7 @@ import java.util.Arrays;
loader.release(this); loader.release(this);
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
released = true; released = true;
hlsSampleStreams.clear();
} }
@Override @Override
@ -750,6 +754,15 @@ import java.util.Arrays;
// Internal methods. // Internal methods.
private void updateSampleStreams(SampleStream[] streams) {
hlsSampleStreams.clear();
for (SampleStream stream : streams) {
if (stream != null) {
hlsSampleStreams.add((HlsSampleStream) stream);
}
}
}
private boolean finishedReadingChunk(HlsMediaChunk chunk) { private boolean finishedReadingChunk(HlsMediaChunk chunk) {
int chunkUid = chunk.uid; int chunkUid = chunk.uid;
int sampleQueueCount = sampleQueues.length; int sampleQueueCount = sampleQueues.length;
@ -807,6 +820,9 @@ import java.util.Arrays;
} }
} }
} }
for (HlsSampleStream sampleStream : hlsSampleStreams) {
sampleStream.bindSampleQueue();
}
} }
/** /**

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls; package com.google.android.exoplayer2.source.hls;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import java.io.IOException; import java.io.IOException;
@ -23,7 +24,7 @@ import java.io.IOException;
public final class SampleQueueMappingException extends IOException { public final class SampleQueueMappingException extends IOException {
/** @param mimeType The mime type of the track group whose mapping failed. */ /** @param mimeType The mime type of the track group whose mapping failed. */
public SampleQueueMappingException(String mimeType) { public SampleQueueMappingException(@Nullable String mimeType) {
super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + ".");
} }
} }

View File

@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper {
playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri);
} }
/** Returns the HLS playlist. Must not be called until after preparation completes. */
public HlsPlaylist getPlaylist() {
Assertions.checkNotNull(playlist);
return playlist;
}
@Override @Override
public int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(playlist); Assertions.checkNotNull(playlist);

View File

@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/ */
public final long startOffsetUs; public final long startOffsetUs;
/** /**
* The start time of the playlist in playback timebase in microseconds. * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.
* Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
* playlist.
*/ */
public final long startTimeUs; public final long startTimeUs;
/** /**

View File

@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return snapshot; return snapshot;
} }
/** Returns the start time of the first loaded primary playlist. */ /**
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
* media playlist has been loaded.
*/
public long getInitialStartTimeUs() { public long getInitialStartTimeUs() {
return initialStartTimeUs; return initialStartTimeUs;
} }
@ -567,7 +570,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal); loadDurationMs, loadable.bytesLoaded(), error, isFatal);
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
boolean shouldRetryIfNotFatal = notifyPlaylistError(playlistUrl, shouldBlacklist); boolean shouldRetryIfNotFatal =
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
if (isFatal) { if (isFatal) {
return Loader.DONT_RETRY_FATAL; return Loader.DONT_RETRY_FATAL;
} }

View File

@ -30,15 +30,11 @@ android {
// testCoverageEnabled = true // testCoverageEnabled = true
// } // }
} }
lintOptions {
lintConfig file("../../checker-framework-lint.xml")
}
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -52,6 +52,12 @@ public final class SsDownloadHelper extends DownloadHelper {
manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri);
} }
/** Returns the SmoothStreaming manifest. Must not be called until after preparation completes. */
public SsManifest getManifest() {
Assertions.checkNotNull(manifest);
return manifest;
}
@Override @Override
public int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(manifest); Assertions.checkNotNull(manifest);

View File

@ -36,6 +36,7 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }
ext { ext {

View File

@ -25,6 +25,7 @@ import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -44,87 +45,83 @@ import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* A time bar that shows a current position, buffered position, duration and ad markers. * A time bar that shows a current position, buffered position, duration and ad markers.
* <p> *
* A DefaultTimeBar can be customized by setting attributes, as outlined below. * <p>A DefaultTimeBar can be customized by setting attributes, as outlined below.
* *
* <h3>Attributes</h3> * <h3>Attributes</h3>
*
* The following attributes can be set on a DefaultTimeBar when used in a layout XML file: * The following attributes can be set on a DefaultTimeBar when used in a layout XML file:
*
* <p> * <p>
*
* <ul> * <ul>
* <li><b>{@code bar_height}</b> - Dimension for the height of the time bar. * <li><b>{@code bar_height}</b> - Dimension for the height of the time bar.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}</li> * <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code touch_target_height}</b> - Dimension for the height of the area in which touch * <li><b>{@code touch_target_height}</b> - Dimension for the height of the area in which touch
* interactions with the time bar are handled. If no height is specified, this also determines * interactions with the time bar are handled. If no height is specified, this also determines
* the height of the view. * the height of the view.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}</li> * <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code ad_marker_width}</b> - Dimension for the width of any ad markers shown on the * <li><b>{@code ad_marker_width}</b> - Dimension for the width of any ad markers shown on the
* bar. Ad markers are superimposed on the time bar to show the times at which ads will play. * bar. Ad markers are superimposed on the time bar to show the times at which ads will play.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}</li> * <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber * <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle * handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle
* should be shown. * should be shown.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}</li> * <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber * <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown. * handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}</li> * <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber * <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown. * handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}</li> * <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_drawable}</b> - Optional reference to a drawable to draw for the * <li><b>{@code scrubber_drawable}</b> - Optional reference to a drawable to draw for the
* scrubber handle. If set, this overrides the default behavior, which is to draw a circle for * scrubber handle. If set, this overrides the default behavior, which is to draw a circle for
* the scrubber handle. * the scrubber handle.
* </li>
* <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media * <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media
* before the current playback position. * before the current playback position.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}</li> * <li>Corresponding method: {@link #setPlayedColor(int)}
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}
* </ul> * </ul>
* </li>
* <li><b>{@code scrubber_color}</b> - Color for the scrubber handle. * <li><b>{@code scrubber_color}</b> - Color for the scrubber handle.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultScrubberColor(int)}</li> * <li>Corresponding method: {@link #setScrubberColor(int)}
* <li>Default: see {@link #getDefaultScrubberColor(int)}
* </ul> * </ul>
* </li>
* <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current * <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current
* played position up to the current buffered position. * played position up to the current buffered position.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultBufferedColor(int)}</li> * <li>Corresponding method: {@link #setBufferedColor(int)}
* <li>Default: see {@link #getDefaultBufferedColor(int)}
* </ul> * </ul>
* </li>
* <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current * <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current
* buffered position. * buffered position.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultUnplayedColor(int)}</li> * <li>Corresponding method: {@link #setUnplayedColor(int)}
* <li>Default: see {@link #getDefaultUnplayedColor(int)}
* </ul> * </ul>
* </li>
* <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers. * <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers.
* <ul> * <ul>
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}</li> * <li>Corresponding method: {@link #setAdMarkerColor(int)}
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}
* </ul> * </ul>
* </li>
* <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers. * <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers.
* <ul> * <ul>
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)}</li> * <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)}
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
* </ul> * </ul>
* </li>
* </ul> * </ul>
*/ */
public class DefaultTimeBar extends View implements TimeBar { public class DefaultTimeBar extends View implements TimeBar {
@ -324,6 +321,72 @@ public class DefaultTimeBar extends View implements TimeBar {
} }
} }
/**
* Sets the color for the portion of the time bar representing media before the playback position.
*
* @param playedColor The color for the portion of the time bar representing media before the
* playback position.
*/
public void setPlayedColor(@ColorInt int playedColor) {
playedPaint.setColor(playedColor);
invalidate(seekBounds);
}
/**
* Sets the color for the scrubber handle.
*
* @param scrubberColor The color for the scrubber handle.
*/
public void setScrubberColor(@ColorInt int scrubberColor) {
scrubberPaint.setColor(scrubberColor);
invalidate(seekBounds);
}
/**
* Sets the color for the portion of the time bar after the current played position up to the
* current buffered position.
*
* @param bufferedColor The color for the portion of the time bar after the current played
* position up to the current buffered position.
*/
public void setBufferedColor(@ColorInt int bufferedColor) {
bufferedPaint.setColor(bufferedColor);
invalidate(seekBounds);
}
/**
* Sets the color for the portion of the time bar after the current played position.
*
* @param unplayedColor The color for the portion of the time bar after the current played
* position.
*/
public void setUnplayedColor(@ColorInt int unplayedColor) {
unplayedPaint.setColor(unplayedColor);
invalidate(seekBounds);
}
/**
* Sets the color for unplayed ad markers.
*
* @param adMarkerColor The color for unplayed ad markers.
*/
public void setAdMarkerColor(@ColorInt int adMarkerColor) {
adMarkerPaint.setColor(adMarkerColor);
invalidate(seekBounds);
}
/**
* Sets the color for played ad markers.
*
* @param playedAdMarkerColor The color for played ad markers.
*/
public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) {
playedAdMarkerPaint.setColor(playedAdMarkerColor);
invalidate(seekBounds);
}
// TimeBar implementation.
@Override @Override
public void addListener(OnScrubListener listener) { public void addListener(OnScrubListener listener) {
listeners.add(listener); listeners.add(listener);
@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar {
update(); update();
} }
// View methods.
@Override @Override
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
super.setEnabled(enabled); super.setEnabled(enabled);
@ -408,8 +473,8 @@ public class DefaultTimeBar extends View implements TimeBar {
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
if (isInSeekBar(x, y)) { if (isInSeekBar(x, y)) {
startScrubbing();
positionScrubber(x); positionScrubber(x);
startScrubbing();
scrubPosition = getScrubberPosition(); scrubPosition = getScrubberPosition();
update(); update();
invalidate(); invalidate();

View File

@ -50,6 +50,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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 * A notification manager to start, update and cancel a media style notification reflecting the
@ -205,7 +206,9 @@ public class PlayerNotificationManager {
new Runnable() { new Runnable() {
@Override @Override
public void run() { public void run() {
if (notificationTag == currentNotificationTag && isNotificationStarted) { if (player != null
&& notificationTag == currentNotificationTag
&& isNotificationStarted) {
updateNotification(bitmap); updateNotification(bitmap);
} }
} }
@ -260,7 +263,7 @@ public class PlayerNotificationManager {
private final String channelId; private final String channelId;
private final int notificationId; private final int notificationId;
private final MediaDescriptionAdapter mediaDescriptionAdapter; private final MediaDescriptionAdapter mediaDescriptionAdapter;
private final CustomActionReceiver customActionReceiver; private final @Nullable CustomActionReceiver customActionReceiver;
private final Handler mainHandler; private final Handler mainHandler;
private final NotificationManagerCompat notificationManager; private final NotificationManagerCompat notificationManager;
private final IntentFilter intentFilter; private final IntentFilter intentFilter;
@ -269,12 +272,12 @@ public class PlayerNotificationManager {
private final Map<String, NotificationCompat.Action> playbackActions; private final Map<String, NotificationCompat.Action> playbackActions;
private final Map<String, NotificationCompat.Action> customActions; private final Map<String, NotificationCompat.Action> customActions;
private Player player; private @Nullable Player player;
private ControlDispatcher controlDispatcher; private ControlDispatcher controlDispatcher;
private boolean isNotificationStarted; private boolean isNotificationStarted;
private int currentNotificationTag; private int currentNotificationTag;
private NotificationListener notificationListener; private @Nullable NotificationListener notificationListener;
private MediaSessionCompat.Token mediaSessionToken; private @Nullable MediaSessionCompat.Token mediaSessionToken;
private boolean useNavigationActions; private boolean useNavigationActions;
private boolean usePlayPauseActions; private boolean usePlayPauseActions;
private @Nullable String stopAction; private @Nullable String stopAction;
@ -365,6 +368,20 @@ public class PlayerNotificationManager {
playerListener = new PlayerListener(); playerListener = new PlayerListener();
notificationBroadcastReceiver = new NotificationBroadcastReceiver(); notificationBroadcastReceiver = new NotificationBroadcastReceiver();
intentFilter = new IntentFilter(); 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 // initialize actions
playbackActions = createPlaybackActions(context); playbackActions = createPlaybackActions(context);
@ -378,22 +395,7 @@ public class PlayerNotificationManager {
for (String action : customActions.keySet()) { for (String action : customActions.keySet()) {
intentFilter.addAction(action); intentFilter.addAction(action);
} }
stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
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);
} }
/** /**
@ -512,10 +514,9 @@ public class PlayerNotificationManager {
} }
this.stopAction = stopAction; this.stopAction = stopAction;
if (ACTION_STOP.equals(stopAction)) { if (ACTION_STOP.equals(stopAction)) {
stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent; stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
} else if (stopAction != null) { } else if (stopAction != null) {
Assertions.checkArgument(customActions.containsKey(stopAction)); stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent;
stopPendingIntent = customActions.get(stopAction).actionIntent;
} else { } else {
stopPendingIntent = null; stopPendingIntent = null;
} }
@ -698,13 +699,15 @@ public class PlayerNotificationManager {
maybeUpdateNotification(); maybeUpdateNotification();
} }
private Notification updateNotification(Bitmap bitmap) { @RequiresNonNull("player")
private Notification updateNotification(@Nullable Bitmap bitmap) {
Notification notification = createNotification(player, bitmap); Notification notification = createNotification(player, bitmap);
notificationManager.notify(notificationId, notification); notificationManager.notify(notificationId, notification);
return notification; return notification;
} }
private void startOrUpdateNotification() { private void startOrUpdateNotification() {
if (player != null) {
Notification notification = updateNotification(null); Notification notification = updateNotification(null);
if (!isNotificationStarted) { if (!isNotificationStarted) {
isNotificationStarted = true; isNotificationStarted = true;
@ -714,9 +717,10 @@ public class PlayerNotificationManager {
} }
} }
} }
}
private void maybeUpdateNotification() { private void maybeUpdateNotification() {
if (isNotificationStarted) { if (isNotificationStarted && player != null) {
updateNotification(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. * 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). // Configure stop action (eg. when user dismisses the notification when !isOngoing).
boolean useStopAction = stopAction != null && !isPlayingAd; boolean useStopAction = stopAction != null && !isPlayingAd;
mediaStyle.setShowCancelButton(useStopAction); mediaStyle.setShowCancelButton(useStopAction);
if (useStopAction) { if (useStopAction && stopPendingIntent != null) {
builder.setDeleteIntent(stopPendingIntent); builder.setDeleteIntent(stopPendingIntent);
mediaStyle.setCancelButtonIntent(stopPendingIntent); mediaStyle.setCancelButtonIntent(stopPendingIntent);
} }
@ -905,7 +851,7 @@ public class PlayerNotificationManager {
if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) { if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) {
stringActions.add(ACTION_NEXT); stringActions.add(ACTION_NEXT);
} }
if (!customActions.isEmpty()) { if (customActionReceiver != null) {
stringActions.addAll(customActionReceiver.getCustomActions(player)); stringActions.addAll(customActionReceiver.getCustomActions(player));
} }
if (ACTION_STOP.equals(stopAction)) { if (ACTION_STOP.equals(stopAction)) {
@ -932,6 +878,64 @@ public class PlayerNotificationManager {
return new int[] {actionIndex}; 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 { private class PlayerListener extends Player.DefaultEventListener {
@Override @Override
@ -946,7 +950,7 @@ public class PlayerNotificationManager {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
if (player.getPlaybackState() == Player.STATE_IDLE) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return; return;
} }
startOrUpdateNotification(); startOrUpdateNotification();
@ -954,7 +958,7 @@ public class PlayerNotificationManager {
@Override @Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
if (player.getPlaybackState() == Player.STATE_IDLE) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return; return;
} }
startOrUpdateNotification(); startOrUpdateNotification();
@ -967,7 +971,7 @@ public class PlayerNotificationManager {
@Override @Override
public void onRepeatModeChanged(int repeatMode) { public void onRepeatModeChanged(int repeatMode) {
if (player.getPlaybackState() == Player.STATE_IDLE) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return; return;
} }
startOrUpdateNotification(); startOrUpdateNotification();
@ -985,7 +989,8 @@ public class PlayerNotificationManager {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (!isNotificationStarted) { Player player = PlayerNotificationManager.this.player;
if (player == null || !isNotificationStarted) {
return; return;
} }
String action = intent.getAction(); String action = intent.getAction();
@ -1013,7 +1018,7 @@ public class PlayerNotificationManager {
} else if (ACTION_STOP.equals(action)) { } else if (ACTION_STOP.equals(action)) {
controlDispatcher.dispatchStop(player, true); controlDispatcher.dispatchStop(player, true);
stopNotification(); stopNotification();
} else if (customActions.containsKey(action)) { } else if (customActionReceiver != null && customActions.containsKey(action)) {
customActionReceiver.onCustomAction(player, action, intent); customActionReceiver.onCustomAction(player, action, intent);
} }
} }

View File

@ -133,6 +133,12 @@ import java.util.List;
* <li>Corresponding method: {@link #setShutterBackgroundColor(int)} * <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
* <li>Default: {@code unset} * <li>Default: {@code unset}
* </ul> * </ul>
* <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame
* or media artwork is kept visible when the player is reset.
* <ul>
* <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
* <li>Default: {@code false}
* </ul>
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below * <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
* for more details. * for more details.
* <ul> * <ul>
@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout {
private boolean useArtwork; private boolean useArtwork;
private Bitmap defaultArtwork; private Bitmap defaultArtwork;
private boolean showBuffering; private boolean showBuffering;
private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage; private @Nullable CharSequence customErrorMessage;
private int controllerShowTimeoutMs; private int controllerShowTimeoutMs;
@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout {
a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering); showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering);
keepContentOnPlayerReset =
a.getBoolean(
R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
controllerHideDuringAds = controllerHideDuringAds =
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
} finally { } finally {
@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout {
if (useController) { if (useController) {
controller.setPlayer(player); controller.setPlayer(player);
} }
if (shutterView != null) {
shutterView.setVisibility(VISIBLE);
}
if (subtitleView != null) { if (subtitleView != null) {
subtitleView.setCues(null); subtitleView.setCues(null);
} }
updateBuffering(); updateBuffering();
updateErrorMessage(); updateErrorMessage();
updateForCurrentTrackSelections(/* isNewPlayer= */ true);
if (player != null) { if (player != null) {
Player.VideoComponent newVideoComponent = player.getVideoComponent(); Player.VideoComponent newVideoComponent = player.getVideoComponent();
if (newVideoComponent != null) { if (newVideoComponent != null) {
@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout {
} }
player.addListener(componentListener); player.addListener(componentListener);
maybeShowController(false); maybeShowController(false);
updateForCurrentTrackSelections();
} else { } else {
hideController(); hideController();
hideArtwork();
} }
} }
@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout {
Assertions.checkState(!useArtwork || artworkView != null); Assertions.checkState(!useArtwork || artworkView != null);
if (this.useArtwork != useArtwork) { if (this.useArtwork != useArtwork) {
this.useArtwork = useArtwork; this.useArtwork = useArtwork;
updateForCurrentTrackSelections(); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
} }
@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout {
public void setDefaultArtwork(Bitmap defaultArtwork) { public void setDefaultArtwork(Bitmap defaultArtwork) {
if (this.defaultArtwork != defaultArtwork) { if (this.defaultArtwork != defaultArtwork) {
this.defaultArtwork = defaultArtwork; this.defaultArtwork = defaultArtwork;
updateForCurrentTrackSelections(); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
} }
@ -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 * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default. * buffering spinner is not displayed by default.
@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout {
return player != null && player.isPlayingAd() && player.getPlayWhenReady(); return player != null && player.isPlayingAd() && player.getPlayWhenReady();
} }
private void updateForCurrentTrackSelections() { private void updateForCurrentTrackSelections(boolean isNewPlayer) {
if (player == null) { if (player == null || player.getCurrentTrackGroups().isEmpty()) {
if (!keepContentOnPlayerReset) {
hideArtwork();
closeShutter();
}
return; return;
} }
if (isNewPlayer && !keepContentOnPlayerReset) {
// Hide any video from the previous player.
closeShutter();
}
TrackSelectionArray selections = player.getCurrentTrackSelections(); TrackSelectionArray selections = player.getCurrentTrackSelections();
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout {
return; return;
} }
} }
// Video disabled so the shutter must be closed. // Video disabled so the shutter must be closed.
if (shutterView != null) { closeShutter();
shutterView.setVisibility(VISIBLE);
}
// Display artwork if enabled and available, else hide it. // Display artwork if enabled and available, else hide it.
if (useArtwork) { if (useArtwork) {
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout {
} }
} }
private void closeShutter() {
if (shutterView != null) {
shutterView.setVisibility(View.VISIBLE);
}
}
private void updateBuffering() { private void updateBuffering() {
if (bufferingView != null) { if (bufferingView != null) {
boolean showBufferingSpinner = boolean showBufferingSpinner =
@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout {
@Override @Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
updateForCurrentTrackSelections(); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
// Player.EventListener implementation // Player.EventListener implementation

View File

@ -372,14 +372,24 @@ import com.google.android.exoplayer2.util.Util;
float previousBottom = layout.getLineTop(0); float previousBottom = layout.getLineTop(0);
int lineCount = layout.getLineCount(); int lineCount = layout.getLineCount();
for (int i = 0; i < lineCount; i++) { for (int i = 0; i < lineCount; i++) {
lineBounds.left = layout.getLineLeft(i) - textPaddingX; float lineTextBoundLeft = layout.getLineLeft(i);
lineBounds.right = layout.getLineRight(i) + textPaddingX; float lineTextBoundRight = layout.getLineRight(i);
lineBounds.left = lineTextBoundLeft - textPaddingX;
lineBounds.right = lineTextBoundRight + textPaddingX;
lineBounds.top = previousBottom; lineBounds.top = previousBottom;
lineBounds.bottom = layout.getLineBottom(i); lineBounds.bottom = layout.getLineBottom(i);
previousBottom = lineBounds.bottom; previousBottom = lineBounds.bottom;
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); canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
} }
} }
}
if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
textPaint.setStrokeJoin(Join.ROUND); textPaint.setStrokeJoin(Join.ROUND);

View File

@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput {
*/ */
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
private static final int FRACTIONAL = 0;
private static final int FRACTIONAL_IGNORE_PADDING = 1;
private static final int ABSOLUTE = 2;
private final List<SubtitlePainter> painters; private final List<SubtitlePainter> painters;
private List<Cue> cues; private List<Cue> cues;
private int textSizeType; private @Cue.TextSizeType int textSizeType;
private float textSize; private float textSize;
private boolean applyEmbeddedStyles; private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes; private boolean applyEmbeddedFontSizes;
@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput {
public SubtitleView(Context context, AttributeSet attrs) { public SubtitleView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
painters = new ArrayList<>(); painters = new ArrayList<>();
textSizeType = FRACTIONAL; textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
textSize = DEFAULT_TEXT_SIZE_FRACTION; textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true; applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true; applyEmbeddedFontSizes = true;
@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput {
} else { } else {
resources = context.getResources(); resources = context.getResources();
} }
setTextSize(ABSOLUTE, TypedValue.applyDimension(unit, size, resources.getDisplayMetrics())); setTextSize(
Cue.TEXT_SIZE_TYPE_ABSOLUTE,
TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
} }
/** /**
@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput {
* height after the top and bottom padding has been subtracted. * height after the top and bottom padding has been subtracted.
*/ */
public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) { public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) {
setTextSize(ignorePadding ? FRACTIONAL_IGNORE_PADDING : FRACTIONAL, fractionOfHeight); setTextSize(
ignorePadding
? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING
: Cue.TEXT_SIZE_TYPE_FRACTIONAL,
fractionOfHeight);
} }
private void setTextSize(int textSizeType, float textSize) { private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) { if (this.textSizeType == textSizeType && this.textSize == textSize) {
return; return;
} }
@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput {
// No space to draw subtitles. // No space to draw subtitles.
return; return;
} }
int rawViewHeight = rawBottom - rawTop;
int viewHeightMinusPadding = bottom - top;
float textSizePx = textSizeType == ABSOLUTE ? textSize float defaultViewTextSizePx =
: textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop)); resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
if (textSizePx <= 0) { if (defaultViewTextSizePx <= 0) {
// Text has no height. // Text has no height.
return; return;
} }
for (int i = 0; i < cueCount; i++) { for (int i = 0; i < cueCount; i++) {
painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, Cue cue = cues.get(i);
textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); float textSizePx =
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
applyEmbeddedStyles,
applyEmbeddedFontSizes,
style,
textSizePx,
bottomPaddingFraction,
canvas,
left,
top,
right,
bottom);
}
}
private float resolveTextSizeForCue(
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return defaultViewTextSizePx;
}
float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx;
}
private float resolveTextSize(
@Cue.TextSizeType int textSizeType,
float textSize,
int rawViewHeight,
int viewHeightMinusPadding) {
switch (textSizeType) {
case Cue.TEXT_SIZE_TYPE_ABSOLUTE:
return textSize;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL:
return textSize * viewHeightMinusPadding;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:
return textSize * rawViewHeight;
case Cue.TYPE_UNSET:
default:
return Cue.DIMEN_UNSET;
} }
} }

View File

@ -21,6 +21,8 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.support.annotation.AttrRes;
import android.support.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair; import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout {
private int rendererIndex; private int rendererIndex;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private boolean isDisabled; private boolean isDisabled;
private SelectionOverride override; private @Nullable SelectionOverride override;
/** /**
* Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
@ -100,11 +102,13 @@ public class TrackSelectionView extends LinearLayout {
this(context, null); this(context, null);
} }
public TrackSelectionView(Context context, AttributeSet attrs) { public TrackSelectionView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0); this(context, attrs, 0);
} }
public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr) { @SuppressWarnings("nullness")
public TrackSelectionView(
Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
TypedArray attributeArray = TypedArray attributeArray =
context context
@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout {
* @param allowAdaptiveSelections Whether adaptive selection is enabled. * @param allowAdaptiveSelections Whether adaptive selection is enabled.
*/ */
public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) { public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {
if (!this.allowAdaptiveSelections == allowAdaptiveSelections) { if (this.allowAdaptiveSelections != allowAdaptiveSelections) {
this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowAdaptiveSelections = allowAdaptiveSelections;
updateViews(); updateViews();
} }
@ -168,12 +172,14 @@ public class TrackSelectionView extends LinearLayout {
} }
/** /**
* Sets the {@link TrackNameProvider} used to generate the user visible name of each track. * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and
* updates the view with track names queried from the specified provider.
* *
* @param trackNameProvider The {@link TrackNameProvider} to use. * @param trackNameProvider The {@link TrackNameProvider} to use.
*/ */
public void setTrackNameProvider(TrackNameProvider trackNameProvider) { public void setTrackNameProvider(TrackNameProvider trackNameProvider) {
this.trackNameProvider = Assertions.checkNotNull(trackNameProvider); this.trackNameProvider = Assertions.checkNotNull(trackNameProvider);
updateViews();
} }
/** /**
@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout {
override = new SelectionOverride(groupIndex, trackIndex); override = new SelectionOverride(groupIndex, trackIndex);
} else { } else {
// An existing override is being modified. // An existing override is being modified.
boolean isEnabled = ((CheckedTextView) view).isChecked();
int overrideLength = override.length; int overrideLength = override.length;
if (isEnabled) { int[] overrideTracks = override.tracks;
if (((CheckedTextView) view).isChecked()) {
// Remove the track from the override. // Remove the track from the override.
if (overrideLength == 1) { if (overrideLength == 1) {
// The last track is being removed, so the override becomes empty. // The last track is being removed, so the override becomes empty.
override = null; override = null;
isDisabled = true; isDisabled = true;
} else { } else {
int[] tracks = getTracksRemoving(override.tracks, trackIndex); int[] tracks = getTracksRemoving(overrideTracks, trackIndex);
override = new SelectionOverride(groupIndex, tracks); override = new SelectionOverride(groupIndex, tracks);
} }
} else { } else {
int[] tracks = getTracksAdding(override.tracks, trackIndex); int[] tracks = getTracksAdding(overrideTracks, trackIndex);
override = new SelectionOverride(groupIndex, tracks); override = new SelectionOverride(groupIndex, tracks);
} }
} }

View File

@ -21,7 +21,7 @@
<string name="exo_track_selection_title_video">Video</string> <string name="exo_track_selection_title_video">Video</string>
<string name="exo_track_selection_title_audio">Audio</string> <string name="exo_track_selection_title_audio">Audio</string>
<string name="exo_track_selection_title_text">Text</string> <string name="exo_track_selection_title_text">Text</string>
<string name="exo_track_selection_none">Keiner</string> <string name="exo_track_selection_none">Ohne</string>
<string name="exo_track_selection_auto">Automatisch</string> <string name="exo_track_selection_auto">Automatisch</string>
<string name="exo_track_unknown">Unbekannt</string> <string name="exo_track_unknown">Unbekannt</string>
<string name="exo_track_resolution">%1$d × %2$d</string> <string name="exo_track_resolution">%1$d × %2$d</string>
@ -31,5 +31,5 @@
<string name="exo_track_surround_5_point_1">5.1-Surround-Sound</string> <string name="exo_track_surround_5_point_1">5.1-Surround-Sound</string>
<string name="exo_track_surround_7_point_1">7.1-Surround-Sound</string> <string name="exo_track_surround_7_point_1">7.1-Surround-Sound</string>
<string name="exo_track_bitrate">%1$.2f Mbit/s</string> <string name="exo_track_bitrate">%1$.2f Mbit/s</string>
<string name="exo_item_list">%1$s und %2$s</string> <string name="exo_item_list">%1$s, %2$s</string>
</resources> </resources>

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