diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5efb382bba..cb3d654e18 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,38 @@ # Release notes # +### 2.8.1 ### + +* HLS: + * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags + ([#4239](https://github.com/google/ExoPlayer/issues/4239)). + * Fix playback of clipped streams starting from non-keyframe positions + ([#4241](https://github.com/google/ExoPlayer/issues/4241)). +* OkHttp extension: Fix to correctly include response headers in thrown + `InvalidResponseCodeException`s. +* Add possibility to cancel `PlayerMessage`s. +* UI components: + * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed + video frame or media artwork visible when the player is reset + ([#2843](https://github.com/google/ExoPlayer/issues/2843)). +* Fix crash when switching surface on Moto E(4) + ([#4134](https://github.com/google/ExoPlayer/issues/4134)). +* Fix a bug that could cause event listeners to be called with inconsistent + information if an event listener interacted with the player + ([#4262](https://github.com/google/ExoPlayer/issues/4262)). +* Audio: + * Fix extraction of PCM in MP4/MOV + ([#4228](https://github.com/google/ExoPlayer/issues/4228)). + * FLAC: Supports seeking for FLAC files without SEEKTABLE + ([#1808](https://github.com/google/ExoPlayer/issues/1808)). +* Captions: + * TTML: + * Fix a styling issue when there are multiple regions displayed at the same + time that can make text size of each region much smaller than defined. + * Fix an issue when the caption line has no text (empty line or only line + break), and the line's background is still displayed. + * Support TTML font size using % correctly (as percentage of document cell + resolution). + ### 2.8.0 ### * Downloading: @@ -75,7 +108,7 @@ * Allow multiple listeners for `DefaultDrmSessionManager`. * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`. * Change minimum API requirement for CBC and pattern encryption from 24 to 25 - ([#4022][https://github.com/google/ExoPlayer/issues/4022]). + ([#4022](https://github.com/google/ExoPlayer/issues/4022)). * Fix handling of 307/308 redirects when making license requests ([#4108](https://github.com/google/ExoPlayer/issues/4108)). * HLS: diff --git a/checker-framework-lint.xml b/checker-framework-lint.xml deleted file mode 100644 index 1d45f9de05..0000000000 --- a/checker-framework-lint.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/constants.gradle b/constants.gradle index dcadcceb4f..9068fb8b56 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.8.0' - releaseVersionCode = 2800 + releaseVersion = '2.8.1' + releaseVersionCode = 2801 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided @@ -33,6 +33,7 @@ project.ext { robolectricVersion = '3.7.1' autoValueVersion = '1.6' checkerframeworkVersion = '2.5.0' + testRunnerVersion = '1.0.2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 3bedefc60e..2232a8b3eb 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ package="com.google.android.exoplayer2.demo"> + diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index 24d815bae2..396f6f8769 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.cast; +import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; @@ -110,7 +111,7 @@ import java.util.Map; // equals and hashCode implementations. @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } else if (!(other instanceof CastTimeline)) { diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fa7ac6b9fa..d5a37db013 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -70,7 +70,8 @@ COMMON_OPTIONS="\ --enable-decoder=flac \ " && \ cd "${FFMPEG_EXT_PATH}/jni" && \ -git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ +(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ +cd ffmpeg && \ ./configure \ --libdir=android-libs/armeabi-v7a \ --arch=arm \ diff --git a/extensions/flac/src/androidTest/assets/bear_no_seek.flac b/extensions/flac/src/androidTest/assets/bear_no_seek.flac new file mode 100644 index 0000000000..cd3271178b Binary files /dev/null and b/extensions/flac/src/androidTest/assets/bear_no_seek.flac differ diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java new file mode 100644 index 0000000000..8124f1958a --- /dev/null +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -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(); + } +} diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java new file mode 100644 index 0000000000..58ab260277 --- /dev/null +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java @@ -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 sampleTimes = expectedTrackOutput.getSampleTimesUs(); + for (int i = 0; i < sampleTimes.size() - 1; i++) { + long currentSampleTime = sampleTimes.get(i); + long nextSampleTime = sampleTimes.get(i + 1); + if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) { + return i; + } + } + return sampleTimes.size() - 1; + } + + private ExtractorInput getExtractorInputFromPosition(long position) throws IOException { + DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null); + dataSource.open(dataSpec); + return new DefaultExtractorInput(dataSource, position, totalInputLength); + } + + private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName) + throws IOException, InterruptedException { + byte[] data = TestUtil.getByteArray(context, fileName); + + FlacExtractor extractor = new FlacExtractor(); + extractor.init(expectedOutput); + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); + + while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {} + } +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java new file mode 100644 index 0000000000..0bbee1ea30 --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.flac; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A {@link SeekMap} implementation for FLAC stream using binary search. + * + *

This seeker performs seeking by using binary search within the stream, until it finds the + * frame that contains the target sample. + */ +/* package */ final class FlacBinarySearchSeeker { + + /** + * When seeking within the source, if the offset is smaller than or equal to this value, the seek + * operation will be performed using a skip operation. Otherwise, the source will be reloaded at + * the new seek position. + */ + private static final long MAX_SKIP_BYTES = 256 * 1024; + + private final FlacStreamInfo streamInfo; + private final FlacBinarySearchSeekMap seekMap; + private final FlacDecoderJni decoderJni; + + private final long firstFramePosition; + private final long inputLength; + private final long approxBytesPerFrame; + + private @Nullable SeekOperationParams pendingSeekOperationParams; + + public FlacBinarySearchSeeker( + FlacStreamInfo streamInfo, + long firstFramePosition, + long inputLength, + FlacDecoderJni decoderJni) { + this.streamInfo = Assertions.checkNotNull(streamInfo); + this.decoderJni = Assertions.checkNotNull(decoderJni); + this.firstFramePosition = firstFramePosition; + this.inputLength = inputLength; + this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame(); + + pendingSeekOperationParams = null; + seekMap = + new FlacBinarySearchSeekMap( + streamInfo, + firstFramePosition, + inputLength, + streamInfo.durationUs(), + approxBytesPerFrame); + } + + /** Returns the seek map for the wrapped FLAC stream. */ + public SeekMap getSeekMap() { + return seekMap; + } + + /** Sets the target time in microseconds within the stream to seek to. */ + public void setSeekTargetUs(long timeUs) { + if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) { + return; + } + + pendingSeekOperationParams = + new SeekOperationParams( + timeUs, + streamInfo.getSampleIndex(timeUs), + /* floorSample= */ 0, + /* ceilingSample= */ streamInfo.totalSamples, + /* floorPosition= */ firstFramePosition, + /* ceilingPosition= */ inputLength, + approxBytesPerFrame); + } + + /** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */ + public boolean hasPendingSeek() { + return pendingSeekOperationParams != null; + } + + /** + * Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from + * {@link Extractor}. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated + * to hold the position of the required seek. + * @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe + * updated to hold the extracted frame that contains the target sample. The caller needs to + * check the byte buffer limit to see if an extracted frame is available. + * @return One of the {@code RESULT_} values defined in {@link Extractor}. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public int handlePendingSeek( + ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer) + throws InterruptedException, IOException { + outputBuffer.position(0); + outputBuffer.limit(0); + while (true) { + long floorPosition = pendingSeekOperationParams.floorPosition; + long ceilingPosition = pendingSeekOperationParams.ceilingPosition; + long searchPosition = pendingSeekOperationParams.nextSearchPosition; + + // streamInfo may not contain minFrameSize, in which case this value will be 0. + int minFrameSize = Math.max(1, streamInfo.minFrameSize); + if (floorPosition + minFrameSize >= ceilingPosition) { + // The seeking range is too small for more than 1 frame, so we can just continue from + // the floor position. + pendingSeekOperationParams = null; + decoderJni.reset(floorPosition); + return seekToPosition(input, floorPosition, seekPositionHolder); + } + + if (!skipInputUntilPosition(input, searchPosition)) { + return seekToPosition(input, searchPosition, seekPositionHolder); + } + + decoderJni.reset(searchPosition); + try { + decoderJni.decodeSampleWithBacktrackPosition( + outputBuffer, /* retryPosition= */ searchPosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + // For some reasons, the extractor can't find a frame mid-stream. + // Stop the seeking and let it re-try playing at the last search position. + pendingSeekOperationParams = null; + throw new IOException("Cannot read frame at position " + searchPosition, e); + } + if (outputBuffer.limit() == 0) { + return Extractor.RESULT_END_OF_INPUT; + } + + long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex(); + long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex(); + long nextFrameSamplePosition = decoderJni.getDecodePosition(); + + boolean targetSampleInLastFrame = + lastFrameSampleIndex <= pendingSeekOperationParams.targetSample + && nextFrameSampleIndex > pendingSeekOperationParams.targetSample; + + if (targetSampleInLastFrame) { + pendingSeekOperationParams = null; + return Extractor.RESULT_CONTINUE; + } + + if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) { + pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition); + } else { + pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition); + } + } + } + + private boolean skipInputUntilPosition(ExtractorInput input, long position) + throws IOException, InterruptedException { + long bytesToSkip = position - input.getPosition(); + if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) { + input.skipFully((int) bytesToSkip); + return true; + } + return false; + } + + private int seekToPosition( + ExtractorInput input, long position, PositionHolder seekPositionHolder) { + if (position == input.getPosition()) { + return Extractor.RESULT_CONTINUE; + } else { + seekPositionHolder.position = position; + return Extractor.RESULT_SEEK; + } + } + + /** + * Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}. + * + *

This class holds parameters for a binary-search for the {@code targetSample} in the range + * [floorPosition, ceilingPosition). + */ + private static final class SeekOperationParams { + private final long seekTimeUs; + private final long targetSample; + private final long approxBytesPerFrame; + private long floorSample; + private long ceilingSample; + private long floorPosition; + private long ceilingPosition; + private long nextSearchPosition; + + private SeekOperationParams( + long seekTimeUs, + long targetSample, + long floorSample, + long ceilingSample, + long floorPosition, + long ceilingPosition, + long approxBytesPerFrame) { + this.seekTimeUs = seekTimeUs; + this.floorSample = floorSample; + this.ceilingSample = ceilingSample; + this.floorPosition = floorPosition; + this.ceilingPosition = ceilingPosition; + this.targetSample = targetSample; + this.approxBytesPerFrame = approxBytesPerFrame; + updateNextSearchPosition(); + } + + /** Updates the floor constraints (inclusive) of the seek operation. */ + private void updateSeekFloor(long floorSample, long floorPosition) { + this.floorSample = floorSample; + this.floorPosition = floorPosition; + updateNextSearchPosition(); + } + + /** Updates the ceiling constraints (exclusive) of the seek operation. */ + private void updateSeekCeiling(long ceilingSample, long ceilingPosition) { + this.ceilingSample = ceilingSample; + this.ceilingPosition = ceilingPosition; + updateNextSearchPosition(); + } + + private void updateNextSearchPosition() { + this.nextSearchPosition = + getNextSearchPosition( + targetSample, + floorSample, + ceilingSample, + floorPosition, + ceilingPosition, + approxBytesPerFrame); + } + + /** + * Returns the next position in FLAC stream to search for target sample, given [floorPosition, + * ceilingPosition). + */ + private static long getNextSearchPosition( + long targetSample, + long floorSample, + long ceilingSample, + long floorPosition, + long ceilingPosition, + long approxBytesPerFrame) { + if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) { + return floorPosition; + } + long samplesToSkip = targetSample - floorSample; + long estimatedBytesPerSample = + Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample)); + // In the stream, the samples are accessed in a group of frame. Given a stream position, the + // seeker will be able to find the first frame following that position. + // Hence, if our target sample is in the middle of a frame, and our estimate position is + // correct, or very near the actual sample position, the seeker will keep accessing the next + // frame, rather than the frame that contains the target sample. + // Moreover, it's better to under-estimate rather than over-estimate, because the extractor + // input can skip forward easily, but cannot rewind easily (it may require a new connection + // to be made). + // Therefore, we should reduce the estimated position by some amount, so it will converge to + // the correct frame earlier. + long bytesToSkip = samplesToSkip * estimatedBytesPerSample; + long confidenceInterval = bytesToSkip / 20; + + long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1); + long estimatedPosition = estimatedFramePosition - confidenceInterval; + + return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1); + } + } + + /** + * A {@link SeekMap} implementation that returns the estimated byte location from {@link + * SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link + * #getSeekPoints(long)} query. + */ + private static final class FlacBinarySearchSeekMap implements SeekMap { + private final FlacStreamInfo streamInfo; + private final long firstFramePosition; + private final long inputLength; + private final long approxBytesPerFrame; + private final long durationUs; + + private FlacBinarySearchSeekMap( + FlacStreamInfo streamInfo, + long firstFramePosition, + long inputLength, + long durationUs, + long approxBytesPerFrame) { + this.streamInfo = streamInfo; + this.firstFramePosition = firstFramePosition; + this.inputLength = inputLength; + this.approxBytesPerFrame = approxBytesPerFrame; + this.durationUs = durationUs; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long nextSearchPosition = + SeekOperationParams.getNextSearchPosition( + streamInfo.getSampleIndex(timeUs), + /* floorSample= */ 0, + /* ceilingSample= */ streamInfo.totalSamples, + /* floorPosition= */ firstFramePosition, + /* ceilingPosition= */ inputLength, + approxBytesPerFrame); + return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition)); + } + + @Override + public long getDurationUs() { + return durationUs; + } + } +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 15d294a35a..e8a04e06ae 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -92,18 +92,14 @@ import java.util.List; } decoderJni.setData(inputBuffer.data); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); - int result; try { - result = decoderJni.decodeSample(outputData); + decoderJni.decodeSample(outputData); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + return new FlacDecoderException("Frame decoding failed", e); } catch (IOException | InterruptedException e) { // Never happens. throw new IllegalStateException(e); } - if (result < 0) { - return new FlacDecoderException("Frame decoding failed"); - } - outputData.position(0); - outputData.limit(result); return null; } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index ce787712da..69c0d082ee 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -26,6 +26,17 @@ import java.nio.ByteBuffer; */ /* package */ final class FlacDecoderJni { + /** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */ + public static final class FlacFrameDecodeException extends Exception { + + public final int errorCode; + + public FlacFrameDecodeException(String message, int errorCode) { + super(message); + this.errorCode = errorCode; + } + } + private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private final long nativeDecoderContext; @@ -116,14 +127,50 @@ import java.nio.ByteBuffer; return byteCount; } + /** Decodes and consumes the StreamInfo section from the FLAC stream. */ public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { return flacDecodeMetadata(nativeDecoderContext); } - public int decodeSample(ByteBuffer output) throws IOException, InterruptedException { - return output.isDirect() - ? flacDecodeToBuffer(nativeDecoderContext, output) - : flacDecodeToArray(nativeDecoderContext, output.array()); + /** + * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO + * error occurs, resets the stream and input to the given {@code retryPosition}. + * + * @param output The byte buffer to hold the decoded frame. + * @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}. + */ + public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition) + throws InterruptedException, IOException, FlacFrameDecodeException { + try { + decodeSample(output); + } catch (IOException e) { + if (retryPosition >= 0) { + reset(retryPosition); + if (extractorInput != null) { + extractorInput.setRetryPosition(retryPosition, e); + } + } + throw e; + } + } + + /** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */ + public void decodeSample(ByteBuffer output) + throws IOException, InterruptedException, FlacFrameDecodeException { + output.clear(); + int frameSize = + output.isDirect() + ? flacDecodeToBuffer(nativeDecoderContext, output) + : flacDecodeToArray(nativeDecoderContext, output.array()); + if (frameSize < 0) { + if (!isDecoderAtEndOfInput()) { + throw new FlacFrameDecodeException("Cannot decode FLAC frame", frameSize); + } + // The decoder has read to EOI. Return a 0-size frame to indicate the EOI. + output.limit(0); + } else { + output.limit(frameSize); + } } /** @@ -133,8 +180,19 @@ import java.nio.ByteBuffer; return flacGetDecodePosition(nativeDecoderContext); } - public long getLastSampleTimestamp() { - return flacGetLastTimestamp(nativeDecoderContext); + /** Returns the timestamp for the first sample in the last decoded frame. */ + public long getLastFrameTimestamp() { + return flacGetLastFrameTimestamp(nativeDecoderContext); + } + + /** Returns the first sample index of the last extracted frame. */ + public long getLastFrameFirstSampleIndex() { + return flacGetLastFrameFirstSampleIndex(nativeDecoderContext); + } + + /** Returns the first sample index of the frame to be extracted next. */ + public long getNextFrameFirstSampleIndex() { + return flacGetNextFrameFirstSampleIndex(nativeDecoderContext); } /** @@ -153,6 +211,11 @@ import java.nio.ByteBuffer; return flacGetStateString(nativeDecoderContext); } + /** Returns whether the decoder has read to the end of the input. */ + public boolean isDecoderAtEndOfInput() { + return flacIsDecoderAtEndOfStream(nativeDecoderContext); + } + public void flush() { flacFlush(nativeDecoderContext); } @@ -181,18 +244,34 @@ import java.nio.ByteBuffer; } private native long flacInit(); + private native FlacStreamInfo flacDecodeMetadata(long context) throws IOException, InterruptedException; + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException, InterruptedException; + private native int flacDecodeToArray(long context, byte[] outputArray) throws IOException, InterruptedException; + private native long flacGetDecodePosition(long context); - private native long flacGetLastTimestamp(long context); + + private native long flacGetLastFrameTimestamp(long context); + + private native long flacGetLastFrameFirstSampleIndex(long context); + + private native long flacGetNextFrameFirstSampleIndex(long context); + private native long flacGetSeekPosition(long context, long timeUs); + private native String flacGetStateString(long context); + + private native boolean flacIsDecoderAtEndOfStream(long context); + private native void flacFlush(long context); + private native void flacReset(long context, long newPosition); + private native void flacRelease(long context); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 34a6e6820d..a5efeb69f9 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor { private ParsableByteArray outputBuffer; private ByteBuffer outputByteBuffer; + private FlacStreamInfo streamInfo; private Metadata id3Metadata; + private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker; - private boolean metadataParsed; + private boolean readPastStreamInfo; /** Constructs an instance with flags = 0. */ public FlacExtractor() { @@ -136,83 +138,43 @@ public final class FlacExtractor implements Extractor { } decoderJni.setData(input); + readPastStreamInfo(input); - if (!metadataParsed) { - final FlacStreamInfo streamInfo; - try { - streamInfo = decoderJni.decodeMetadata(); - if (streamInfo == null) { - throw new IOException("Metadata decoding failed"); - } - } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); - throw e; // never executes - } - metadataParsed = true; - - boolean isSeekable = decoderJni.getSeekPosition(0) != -1; - extractorOutput.seekMap( - isSeekable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) - : new SeekMap.Unseekable(streamInfo.durationUs(), 0)); - Format mediaFormat = - Format.createAudioSampleFormat( - /* id= */ null, - MimeTypes.AUDIO_RAW, - /* codecs= */ null, - streamInfo.bitRate(), - streamInfo.maxDecodedFrameSize(), - streamInfo.channels, - streamInfo.sampleRate, - getPcmEncoding(streamInfo.bitsPerSample), - /* encoderDelay= */ 0, - /* encoderPadding= */ 0, - /* initializationData= */ null, - /* drmInitData= */ null, - /* selectionFlags= */ 0, - /* language= */ null, - isId3MetadataDisabled ? null : id3Metadata); - trackOutput.format(mediaFormat); - - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); - outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); + if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) { + return handlePendingSeek(input, seekPosition); } - outputBuffer.reset(); long lastDecodePosition = decoderJni.getDecodePosition(); - int size; try { - size = decoderJni.decodeSample(outputByteBuffer); - } catch (IOException e) { - if (lastDecodePosition >= 0) { - decoderJni.reset(lastDecodePosition); - input.setRetryPosition(lastDecodePosition, e); - } - throw e; + decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + throw new IOException("Cannot read frame at position " + lastDecodePosition, e); } - if (size <= 0) { + int outputSize = outputByteBuffer.limit(); + if (outputSize == 0) { return RESULT_END_OF_INPUT; } - trackOutput.sampleData(outputBuffer, size); - trackOutput.sampleMetadata(decoderJni.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size, - 0, null); + writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } @Override public void seek(long position, long timeUs) { if (position == 0) { - metadataParsed = false; + readPastStreamInfo = false; } if (decoderJni != null) { decoderJni.reset(position); } + if (flacBinarySearchSeeker != null) { + flacBinarySearchSeeker.setSeekTargetUs(timeUs); + } } @Override public void release() { + flacBinarySearchSeeker = null; if (decoderJni != null) { decoderJni.release(); decoderJni = null; @@ -244,6 +206,100 @@ public final class FlacExtractor implements Extractor { return Arrays.equals(header, FLAC_SIGNATURE); } + private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { + if (readPastStreamInfo) { + return; + } + + FlacStreamInfo streamInfo = decodeStreamInfo(input); + readPastStreamInfo = true; + if (this.streamInfo == null) { + updateFlacStreamInfo(input, streamInfo); + } + } + + private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) { + this.streamInfo = streamInfo; + outputSeekMap(input, streamInfo); + outputFormat(streamInfo); + outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); + } + + private FlacStreamInfo decodeStreamInfo(ExtractorInput input) + throws InterruptedException, IOException { + try { + FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); + if (streamInfo == null) { + throw new IOException("Metadata decoding failed"); + } + return streamInfo; + } catch (IOException e) { + decoderJni.reset(0); + input.setRetryPosition(0, e); + throw e; + } + } + + private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { + boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; + SeekMap seekMap = + hasSeekTable + ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) + : getSeekMapForNonSeekTableFlac(input, streamInfo); + extractorOutput.seekMap(seekMap); + } + + private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET) { + long firstFramePosition = decoderJni.getDecodePosition(); + flacBinarySearchSeeker = + new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); + return flacBinarySearchSeeker.getSeekMap(); + } else { // can't seek at all, because there's no SeekTable and the input length is unknown. + return new SeekMap.Unseekable(streamInfo.durationUs()); + } + } + + private void outputFormat(FlacStreamInfo streamInfo) { + Format mediaFormat = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + streamInfo.bitRate(), + streamInfo.maxDecodedFrameSize(), + streamInfo.channels, + streamInfo.sampleRate, + getPcmEncoding(streamInfo.bitsPerSample), + /* encoderDelay= */ 0, + /* encoderPadding= */ 0, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + isId3MetadataDisabled ? null : id3Metadata); + trackOutput.format(mediaFormat); + } + + private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) + throws InterruptedException, IOException { + int seekResult = + flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer); + if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { + writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp()); + } + return seekResult; + } + + private void writeLastSampleToOutput(int size, long lastSampleTimestamp) { + outputBuffer.setPosition(0); + trackOutput.sampleData(outputBuffer, size); + trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + } + + /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ private static final class FlacSeekMap implements SeekMap { private final long durationUs; diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 59f37b0c2e..298719d48d 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) { return context->parser->getDecodePosition(); } -DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) { +DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) { Context *context = reinterpret_cast(jContext); - return context->parser->getLastTimestamp(); + return context->parser->getLastFrameTimestamp(); +} + +DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->getLastFrameFirstSampleIndex(); +} + +DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->getNextFrameFirstSampleIndex(); } DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { @@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) { return env->NewStringUTF(str); } +DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->isDecoderAtEndOfStream(); +} + DECODER_FUNC(void, flacFlush, jlong jContext) { Context *context = reinterpret_cast(jContext); context->parser->flush(); diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index 8a769b66d4..cea7fbe33b 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -44,10 +44,18 @@ class FLACParser { return mStreamInfo; } - int64_t getLastTimestamp() const { + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } + int64_t getLastFrameFirstSampleIndex() const { + return mWriteHeader.number.sample_number; + } + + int64_t getNextFrameFirstSampleIndex() const { + return mWriteHeader.number.sample_number + mWriteHeader.blocksize; + } + bool decodeMetadata(); size_t readBuffer(void *output, size_t output_size); @@ -83,6 +91,11 @@ class FLACParser { return FLAC__stream_decoder_get_resolved_state_string(mDecoder); } + bool isDecoderAtEndOfStream() const { + return FLAC__stream_decoder_get_state(mDecoder) == + FLAC__STREAM_DECODER_END_OF_STREAM; + } + private: DataSource *mDataSource; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index d3dbaaec96..2d9ddfb288 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -649,18 +649,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void loadAd(String adUriString) { - if (adGroupIndex == C.INDEX_UNSET) { - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); - } - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } try { + if (adGroupIndex == C.INDEX_UNSET) { + Log.w( + TAG, + "Unexpected loadAd without LOADED event; assuming ad group index is actually " + + expectedAdGroupIndex); + adGroupIndex = expectedAdGroupIndex; + adsManager.start(); + } + if (DEBUG) { + Log.d(TAG, "loadAd in ad group " + adGroupIndex); + } int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); if (adIndexInAdGroup == C.INDEX_UNSET) { Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 172159b7af..f2898005c1 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource { // Check for a valid response code. if (!response.isSuccessful()) { - Map> headers = request.headers().toMultimap(); + Map> headers = response.headers().toMultimap(); closeConnectionQuietly(); InvalidResponseCodeException exception = new InvalidResponseCodeException( responseCode, headers, dataSpec); diff --git a/library/core/build.gradle b/library/core/build.gradle index 52249220e0..bb331b615c 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -22,6 +22,13 @@ android { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + // The following argument makes the Android Test Orchestrator run its + // "pm clear" command after each test invocation. This command ensures + // that the app's state is completely cleared between tests. + testInstrumentationRunnerArguments clearPackageData: 'true' } // Workaround to prevent circular dependency on project :testutils. @@ -42,19 +49,17 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion + androidTestImplementation 'com.android.support.test:runner:' + testRunnerVersion + androidTestUtil 'com.android.support.test:orchestrator:' + testRunnerVersion testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'junit:junit:' + junitVersion testImplementation 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 3465393853..1133928e91 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.fail; -import android.app.Instrumentation; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; @@ -28,48 +28,58 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.test.InstrumentationTestCase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Unit tests for {@link ContentDataSource}. - */ -public final class ContentDataSourceTest extends InstrumentationTestCase { +/** Unit tests for {@link ContentDataSource}. */ +@RunWith(AndroidJUnit4.class) +public final class ContentDataSourceTest { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + @Test public void testRead() throws Exception { - assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); + assertData(0, C.LENGTH_UNSET, false); } + @Test public void testReadPipeMode() throws Exception { - assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); + assertData(0, C.LENGTH_UNSET, true); } + @Test public void testReadFixedLength() throws Exception { - assertData(getInstrumentation(), 0, 100, false); + assertData(0, 100, false); } + @Test public void testReadFromOffsetToEndOfInput() throws Exception { - assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); + assertData(1, C.LENGTH_UNSET, false); } + @Test public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { - assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); + assertData(1, C.LENGTH_UNSET, true); } + @Test public void testReadFromOffsetFixedLength() throws Exception { - assertData(getInstrumentation(), 1, 100, false); + assertData(1, 100, false); } + @Test public void testReadInvalidUri() throws Exception { - ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + ContentDataSource dataSource = + new ContentDataSource(InstrumentationRegistry.getTargetContext()); Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } - private static void assertData(Instrumentation instrumentation, int offset, int length, - boolean pipeMode) throws IOException { + private static void assertData(int offset, int length, boolean pipeMode) throws IOException { Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); - ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); + ContentDataSource dataSource = + new ContentDataSource(InstrumentationRegistry.getTargetContext()); try { DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); - byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH); + byte[] completeData = + TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH); byte[] expectedData = Arrays.copyOfRange(completeData, offset, length == C.LENGTH_UNSET ? completeData.length : offset + length); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 58531346ab..be4a2a96dc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import android.net.Uri; -import android.test.InstrumentationTestCase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; @@ -29,9 +30,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Set; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; /** Tests {@link CachedContentIndex}. */ -public class CachedContentIndexTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class CachedContentIndexTest { private final byte[] testIndexV1File = { 0, 0, 0, 1, // version @@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase { private CachedContentIndex index; private File cacheDir; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cacheDir = + Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testAddGetRemove() throws Exception { final String key1 = "key1"; final String key2 = "key2"; @@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(cacheSpanFile.exists()).isTrue(); } + @Test public void testStoreAndLoad() throws Exception { assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir)); } + @Test public void testLoadV1() throws Exception { FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); fos.write(testIndexV1File); @@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } + @Test public void testLoadV2() throws Exception { FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); fos.write(testIndexV2File); @@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); } - public void testAssignIdForKeyAndGetKeyForId() throws Exception { + @Test + public void testAssignIdForKeyAndGetKeyForId() { final String key1 = "key1"; final String key2 = "key2"; int id1 = index.assignIdForKey(key1); @@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.assignIdForKey(key2)).isEqualTo(id2); } - public void testGetNewId() throws Exception { + @Test + public void testGetNewId() { SparseArray idToKey = new SparseArray<>(); assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0); idToKey.put(10, ""); @@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1); } + @Test public void testEncryption() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key @@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key)); } - public void testRemoveEmptyNotLockedCachedContent() throws Exception { + @Test + public void testRemoveEmptyNotLockedCachedContent() { CachedContent cachedContent = index.getOrAdd("key1"); index.maybeRemove(cachedContent.key); @@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.get(cachedContent.key)).isNull(); } + @Test public void testCantRemoveNotEmptyCachedContent() throws Exception { CachedContent cachedContent = index.getOrAdd("key1"); File cacheSpanFile = @@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase { assertThat(index.get(cachedContent.key)).isNotNull(); } - public void testCantRemoveLockedCachedContent() throws Exception { + @Test + public void testCantRemoveLockedCachedContent() { CachedContent cachedContent = index.getOrAdd("key1"); cachedContent.setLocked(true); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 637a19cdd2..afbbf6605f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import android.test.InstrumentationTestCase; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileOutputStream; @@ -26,11 +27,14 @@ import java.io.IOException; import java.util.HashMap; import java.util.Set; import java.util.TreeSet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; -/** - * Unit tests for {@link SimpleCacheSpan}. - */ -public class SimpleCacheSpanTest extends InstrumentationTestCase { +/** Unit tests for {@link SimpleCacheSpan}. */ +@RunWith(AndroidJUnit4.class) +public class SimpleCacheSpanTest { private CachedContentIndex index; private File cacheDir; @@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { return SimpleCacheSpan.createCacheEntry(cacheFile, index); } - @Override - protected void setUp() throws Exception { - super.setUp(); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + @Before + public void setUp() throws Exception { + cacheDir = + Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testCacheFile() throws Exception { assertCacheSpan("key1", 0, 0); assertCacheSpan("key2", 1, 2); @@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { + "A paragraph-separator character \u2029", 1, 2); } + @Test public void testUpgradeFileName() throws Exception { String key = "asd\u00aa"; int id = index.assignIdForKey(key); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index b5b364a327..f8b7f5f5c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -46,11 +46,10 @@ public class DefaultLoadControl implements LoadControl { public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, - * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user - * action. + * The default duration of media that must be buffered for playback to resume after a rebuffer, in + * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; /** * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 6d8dd5b7a8..39a6243933 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -185,10 +185,6 @@ public interface ExoPlayer extends Player { */ Looper getPlaybackLooper(); - @Override - @Nullable - ExoPlaybackException getPlaybackError(); - /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to * {@code prepare(mediaSource, true, true)}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 5ca5994b6e..4125a203a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (this.playWhenReady != playWhenReady) { this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); + PlaybackInfo playbackInfo = this.playbackInfo; for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); } @@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; case ExoPlayerImplInternal.MSG_ERROR: - playbackError = (ExoPlaybackException) msg.obj; + ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj; + this.playbackError = playbackError; for (Player.EventListener listener : listeners) { listener.onPlayerError(playbackError); } @@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet; boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; boolean trackSelectorResultChanged = - this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; + playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; playbackInfo = newPlaybackInfo; if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { for (Player.EventListener listener : listeners) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index ceee25af82..fc946804f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -854,6 +854,9 @@ import java.util.Collections; } private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { + if (message.isCanceled()) { + return; + } try { message.getTarget().handleMessage(message.getType(), message.getPayload()); } finally { @@ -945,7 +948,7 @@ import java.util.Collections; && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { sendMessageToTarget(nextInfo.message); - if (nextInfo.message.getDeleteAfterDelivery()) { + if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { pendingMessages.remove(nextPendingMessageIndex); } else { nextPendingMessageIndex++; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 98d5fe91b7..aabb01481b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.8.0"; + public static final String VERSION = "2.8.1"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2008000; + public static final int VERSION_INT = 2008001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index a7de96a2de..6f2db4ff5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; /** @@ -87,7 +88,7 @@ public final class PlaybackParameters { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 408cbecaf1..2c7aee834e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -63,6 +63,7 @@ public final class PlayerMessage { private boolean isSent; private boolean isDelivered; private boolean isProcessed; + private boolean isCanceled; /** * Creates a new message. @@ -242,6 +243,24 @@ public final class PlayerMessage { return this; } + /** + * Cancels the message delivery. + * + * @return This message. + * @throws IllegalStateException If this method is called before {@link #send()}. + */ + public synchronized PlayerMessage cancel() { + Assertions.checkState(isSent); + isCanceled = true; + markAsProcessed(/* isDelivered= */ false); + return this; + } + + /** Returns whether the message delivery has been canceled. */ + public synchronized boolean isCanceled() { + return isCanceled; + } + /** * Blocks until after the message has been delivered or the player is no longer able to deliver * the message. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java index 93bbd1e4b6..684072efc6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererConfiguration.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; + /** * The configuration of a {@link Renderer}. */ @@ -41,7 +43,7 @@ public final class RendererConfiguration { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java index 2df9840cf8..ca0433f96d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; /** @@ -71,7 +72,7 @@ public final class SeekParameters { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 482e2c970a..0a0df03053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private AudioAttributes audioAttributes; private float audioVolume; private MediaSource mediaSource; + private List currentCues; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. @@ -177,6 +178,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioAttributes = AudioAttributes.DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + currentCues = Collections.emptyList(); // Build the player and associated objects. player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); @@ -502,6 +504,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void addTextOutput(TextOutput listener) { + if (!currentCues.isEmpty()) { + listener.onCues(currentCues); + } textOutputs.add(listener); } @@ -775,6 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player mediaSource = null; analyticsCollector.resetForNewMediaSource(); } + currentCues = Collections.emptyList(); } @Override @@ -790,6 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); } + currentCues = Collections.emptyList(); } @Override @@ -1095,6 +1102,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void onCues(List cues) { + currentCues = cues; for (TextOutput textOutput : textOutputs) { textOutput.onCues(cues); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index 337200da8f..5e963a2540 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.audio; import android.annotation.TargetApi; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; /** @@ -119,7 +120,7 @@ public final class AudioAttributes { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java index 499ea488c7..4b03a5047b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioCapabilities.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.AudioFormat; import android.media.AudioManager; +import android.support.annotation.Nullable; import java.util.Arrays; /** @@ -96,7 +97,7 @@ public final class AudioCapabilities { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (this == other) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 4a59667dc8..c2de662010 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator, Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator, Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (!(obj instanceof SchemeData)) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index aa718c23e5..b7aaa2a31b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; @@ -92,7 +93,7 @@ public interface SeekMap { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java index 93cfbd9200..8b920bc024 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; + /** Defines a seek point in a media stream. */ public final class SeekPoint { @@ -42,7 +44,7 @@ public final class SeekPoint { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index a12a0315a4..6a8cef6b64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -69,7 +70,7 @@ public interface TrackOutput { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index a6e2524f0b..a2b787d6b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -189,11 +189,13 @@ import java.util.List; } } - // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. - boolean isRechunkable = sampleSizeBox.isFixedSampleSize() - && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) - && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0 - && remainingSynchronizationSamples == 0; + // Fixed sample size raw audio may need to be rechunked. + boolean isFixedSampleSizeRawAudio = + sampleSizeBox.isFixedSampleSize() + && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType) + && remainingTimestampDeltaChanges == 0 + && remainingTimestampOffsetChanges == 0 + && remainingSynchronizationSamples == 0; long[] offsets; int[] sizes; @@ -203,7 +205,7 @@ import java.util.List; long timestampTimeUnits = 0; long duration; - if (!isRechunkable) { + if (!isFixedSampleSizeRawAudio) { offsets = new long[sampleCount]; sizes = new int[sampleCount]; timestamps = new long[sampleCount]; @@ -296,7 +298,8 @@ import java.util.List; chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; } - int fixedSampleSize = sampleSizeBox.readNextSampleSize(); + int fixedSampleSize = + Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount); FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); offsets = rechunkedResults.offsets; @@ -1224,7 +1227,7 @@ import java.util.List; stsc.setPosition(Atom.FULL_HEADER_SIZE); remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); - index = C.INDEX_UNSET; + index = -1; } public boolean moveNext() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 49f7361bc5..347afe29fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -482,13 +482,13 @@ public final class MediaCodecUtil { return null; } - Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger); - if (profile == null) { + int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { Log.w(TAG, "Unknown AVC profile: " + profileInteger); return null; } - Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger); - if (level == null) { + int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { Log.w(TAG, "Unknown AVC level: " + levelInteger); return null; } @@ -639,7 +639,7 @@ public final class MediaCodecUtil { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index a8c9d0b5a8..a2ad7fe2ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import java.util.Arrays; import java.util.List; @@ -76,7 +77,7 @@ public final class Metadata implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index 0612c18e18..5f521aada6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index eafb0286ce..ae78f712c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java index f662c1d06f..129803299c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import java.util.Arrays; /** @@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java index c82f982aa7..aca530cdee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java index 939c00b9db..56b08bbee3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java index b43a46349c..e84b776790 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** @@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java index 0ed429055b..8b665fce00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java index db6db2ea4f..1b5ba67c11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 3374db5d8d..dbab4ca7a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** @@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 775ab5dd3e..f657eefc30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** @@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index cf061f3745..98360b909c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -140,7 +140,7 @@ public abstract class DownloadAction { DownloaderConstructorHelper downloaderConstructorHelper); @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (o == null || getClass() != o.getClass()) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 8be822b6ca..0e2c5874b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -250,7 +251,6 @@ public final class DownloadManager { Assertions.checkState(!released); Task task = addTaskForAction(action); if (initialized) { - notifyListenersTaskStateChange(task); saveActions(); maybeStartTasks(); if (task.currentState == STATE_QUEUED) { @@ -413,7 +413,6 @@ public final class DownloadManager { if (released) { return; } - logd("Task state is changed", task); boolean stopped = !task.isActive(); if (stopped) { activeDownloadTasks.remove(task); @@ -430,6 +429,7 @@ public final class DownloadManager { } private void notifyListenersTaskStateChange(Task task) { + logd("Task state is changed", task); TaskState taskState = task.getDownloadState(); for (Listener listener : listeners) { listener.onTaskStateChanged(this, taskState); @@ -468,18 +468,16 @@ public final class DownloadManager { listener.onInitialized(DownloadManager.this); } if (!pendingTasks.isEmpty()) { - for (int i = 0; i < pendingTasks.size(); i++) { - tasks.add(pendingTasks.get(i)); - } + tasks.addAll(pendingTasks); saveActions(); } maybeStartTasks(); - for (int i = 0; i < pendingTasks.size(); i++) { - Task pendingTask = pendingTasks.get(i); - if (pendingTask.currentState == STATE_QUEUED) { + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.get(i); + if (task.currentState == STATE_QUEUED) { // Task did not change out of its initial state, and so its initial state // won't have been reported to listeners. Do so now. - notifyListenersTaskStateChange(pendingTask); + notifyListenersTaskStateChange(task); } } } @@ -699,9 +697,19 @@ public final class DownloadManager { + ' ' + (action.isRemoveAction ? "remove" : "download") + ' ' + + toString(action.data) + + ' ' + getStateString(); } + private static String toString(byte[] data) { + if (data.length > 100) { + return ""; + } else { + return '\'' + Util.fromUtf8Bytes(data) + '\''; + } + } + private String getStateString() { switch (currentState) { case STATE_QUEUED_CANCELING: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java index 02ef7a7aa7..d8db6f96c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java @@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java index f6a32a1253..ae57131641 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -112,7 +112,7 @@ public abstract class SegmentDownloadAction> extends Dow protected abstract void writeKey(DataOutputStream output, K key) throws IOException; @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index f8c2f8b3e1..1a243a8bf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -145,7 +145,7 @@ public interface MediaSource { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index 2e5b259a88..a9fb261768 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; @@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index 72afa3463e..a155032a9f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.Arrays; @@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 5ae1f35b7e..8bc0b8e136 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -78,6 +78,25 @@ public class Cue { */ public static final int LINE_TYPE_NUMBER = 1; + /** The type of default text size for this cue, which may be unset. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_UNSET, + TEXT_SIZE_TYPE_FRACTIONAL, + TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, + TEXT_SIZE_TYPE_ABSOLUTE + }) + public @interface TextSizeType {} + + /** Text size is measured as a fraction of the viewport size minus the view padding. */ + public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0; + + /** Text size is measured as a fraction of the viewport size, ignoring the view padding */ + public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1; + + /** Text size is measured in number of pixels. */ + public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2; + /** * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. @@ -106,40 +125,39 @@ public class Cue { /** * The type of the {@link #line} value. - *

- * {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the + * + *

{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the * viewport. - *

- * {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each - * line is taken to be the size of the first line of the cue. When {@link #line} is greater than - * or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from - * the start edge. When {@link #line} is negative lines count from the end of the viewport, with - * -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height - * of the first line of the cue, and the start and end of the viewport are the top and bottom - * respectively. - *

- * Note that it's particularly important to consider the effect of {@link #lineAnchor} when using - * {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a - * (potentially multi-line) cue at the very top of the viewport. - * {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue - * at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} - * and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of - * the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only - * the last line is visible at the top of the viewport. - * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first - * line is visible at the bottom of the viewport. + * + *

{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of + * each line is taken to be the size of the first line of the cue. When {@link #line} is greater + * than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset + * from the start edge. When {@link #line} is negative lines count from the end of the viewport, + * with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the + * height of the first line of the cue, and the start and end of the viewport are the top and + * bottom respectively. + * + *

Note that it's particularly important to consider the effect of {@link #lineAnchor} when + * using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} + * positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of + * the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 && + * lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line + * == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible + * at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a + * cue so that only its first line is visible at the bottom of the viewport. */ - @LineType public final int lineType; + public final @LineType int lineType; /** - * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - *

- * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} - * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box - * respectively. + * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * + *

For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of + * the cue box respectively. */ - @AnchorType public final int lineAnchor; + public final @AnchorType int lineAnchor; /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in @@ -152,14 +170,14 @@ public class Cue { public final float position; /** - * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. - *

- * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} - * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box - * respectively. + * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * + *

For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of + * the cue box respectively. */ - @AnchorType public final int positionAnchor; + public final @AnchorType int positionAnchor; /** * The size of the cue box in the writing direction specified as a fraction of the viewport size @@ -184,6 +202,18 @@ public class Cue { */ public final int windowColor; + /** + * The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no + * default text size. + */ + public final @TextSizeType int textSizeType; + + /** + * The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default + * text size. + */ + public final float textSize; + /** * Creates an image cue. * @@ -194,17 +224,36 @@ public class Cue { * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a * fraction of the viewport height. - * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, - * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link + * #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. * @param width The width of the cue as a fraction of the viewport width. - * @param height The height of the cue as a fraction of the viewport height, or - * {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the - * specified {@code width}. + * @param height The height of the cue as a fraction of the viewport height, or {@link + * #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified + * {@code width}. */ - public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, - float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) { - this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, - horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK); + public Cue( + Bitmap bitmap, + float horizontalPosition, + @AnchorType int horizontalPositionAnchor, + float verticalPosition, + @AnchorType int verticalPositionAnchor, + float width, + float height) { + this( + /* text= */ null, + /* textAlignment= */ null, + bitmap, + verticalPosition, + /* lineType= */ LINE_TYPE_FRACTION, + verticalPositionAnchor, + horizontalPosition, + horizontalPositionAnchor, + /* textSizeType= */ TYPE_UNSET, + /* textSize= */ DIMEN_UNSET, + width, + height, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); } /** @@ -214,7 +263,15 @@ public class Cue { * @param text See {@link #text}. */ public Cue(CharSequence text) { - this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); + this( + text, + /* textAlignment= */ null, + /* line= */ DIMEN_UNSET, + /* lineType= */ TYPE_UNSET, + /* lineAnchor= */ TYPE_UNSET, + /* position= */ DIMEN_UNSET, + /* positionAnchor= */ TYPE_UNSET, + /* size= */ DIMEN_UNSET); } /** @@ -229,10 +286,68 @@ public class Cue { * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. */ - public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { - this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, - Color.BLACK); + public Cue( + CharSequence text, + Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size) { + this( + text, + textAlignment, + line, + lineType, + lineAnchor, + position, + positionAnchor, + size, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); + } + + /** + * Creates a text cue. + * + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param textSizeType See {@link #textSizeType}. + * @param textSize See {@link #textSize}. + */ + public Cue( + CharSequence text, + Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + @TextSizeType int textSizeType, + float textSize) { + this( + text, + textAlignment, + /* bitmap= */ null, + line, + lineType, + lineAnchor, + position, + positionAnchor, + textSizeType, + textSize, + size, + /* bitmapHeight= */ DIMEN_UNSET, + /* windowColorSet= */ false, + /* windowColor= */ Color.BLACK); } /** @@ -249,16 +364,48 @@ public class Cue { * @param windowColorSet See {@link #windowColorSet}. * @param windowColor See {@link #windowColor}. */ - public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - boolean windowColorSet, int windowColor) { - this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, - DIMEN_UNSET, windowColorSet, windowColor); + public Cue( + CharSequence text, + Alignment textAlignment, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + boolean windowColorSet, + int windowColor) { + this( + text, + textAlignment, + /* bitmap= */ null, + line, + lineType, + lineAnchor, + position, + positionAnchor, + /* textSizeType= */ TYPE_UNSET, + /* textSize= */ DIMEN_UNSET, + size, + /* bitmapHeight= */ DIMEN_UNSET, + windowColorSet, + windowColor); } - private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, - @LineType int lineType, @AnchorType int lineAnchor, float position, - @AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet, + private Cue( + CharSequence text, + Alignment textAlignment, + Bitmap bitmap, + float line, + @LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + @TextSizeType int textSizeType, + float textSize, + float size, + float bitmapHeight, + boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; @@ -272,6 +419,8 @@ public class Cue { this.bitmapHeight = bitmapHeight; this.windowColorSet = windowColorSet; this.windowColor = windowColor; + this.textSizeType = textSizeType; + this.textSize = textSize; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index a215bf3cc9..ad8f849c60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory; /** * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features * supported by this decoder are: + * *

    *
  • content *
  • core @@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory; *
  • time-clock *
  • time-offset-with-frames *
  • time-offset-with-ticks + *
  • cell-resolution *
+ * * @see TTML specification */ public final class TtmlDecoder extends SimpleSubtitleDecoder { @@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern PERCENTAGE_COORDINATES = Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); + private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$"); private static final int DEFAULT_FRAME_RATE = 30; private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE = new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1); + private static final CellResolution DEFAULT_CELL_RESOLUTION = + new CellResolution(/* columns= */ 32, /* rows= */ 15); private final XmlPullParserFactory xmlParserFactory; @@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; + CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; while (eventType != XmlPullParser.END_DOCUMENT) { TtmlNode parent = nodeStack.peekLast(); if (unsupportedNodeDepth == 0) { @@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { if (eventType == XmlPullParser.START_TAG) { if (TtmlNode.TAG_TT.equals(name)) { frameAndTickRate = parseFrameAndTickRates(xmlParser); + cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION); } if (!isSupportedTag(name)) { Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); unsupportedNodeDepth++; } else if (TtmlNode.TAG_HEAD.equals(name)) { - parseHeader(xmlParser, globalStyles, regionMap); + parseHeader(xmlParser, globalStyles, regionMap, cellResolution); } else { try { TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); @@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate); } - private Map parseHeader(XmlPullParser xmlParser, - Map globalStyles, Map globalRegions) + private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue) + throws SubtitleDecoderException { + String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution"); + if (cellResolution == null) { + return defaultValue; + } + + Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution); + if (!cellResolutionMatcher.matches()) { + Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution); + return defaultValue; + } + try { + int columns = Integer.parseInt(cellResolutionMatcher.group(1)); + int rows = Integer.parseInt(cellResolutionMatcher.group(2)); + if (columns == 0 || rows == 0) { + throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows); + } + return new CellResolution(columns, rows); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution); + return defaultValue; + } + } + + private Map parseHeader( + XmlPullParser xmlParser, + Map globalStyles, + Map globalRegions, + CellResolution cellResolution) throws IOException, XmlPullParserException { do { xmlParser.next(); @@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { globalStyles.put(style.getId(), style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { - TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); + TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution); if (ttmlRegion != null) { globalRegions.put(ttmlRegion.id, ttmlRegion); } @@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /** * Parses a region declaration. - *

- * If the region defines an origin and extent, it is required that they're defined as percentages - * of the viewport. Region declarations that define origin and extent in other formats are - * unsupported, and null is returned. + * + *

If the region defines an origin and extent, it is required that they're defined as + * percentages of the viewport. Region declarations that define origin and extent in other formats + * are unsupported, and null is returned. */ - private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { + private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); if (regionId == null) { return null; @@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } - return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width); + float regionTextHeight = 1.0f / cellResolution.rows; + return new TtmlRegion( + regionId, + position, + line, + /* lineType= */ Cue.LINE_TYPE_FRACTION, + lineAnchor, + width, + /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, + /* textSize= */ regionTextHeight); } private String[] parseStyleIds(String parentStyleIds) { @@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { this.tickRate = tickRate; } } + + /** Represents the cell resolution for a TTML file. */ + private static final class CellResolution { + final int columns; + final int rows; + + CellResolution(int columns, int rows) { + this.columns = columns; + this.rows = rows; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 43fa7a1bd9..c8b9a59de4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -175,35 +175,51 @@ import java.util.TreeSet; Map regionMap) { TreeMap regionOutputs = new TreeMap<>(); traverseForText(timeUs, false, regionId, regionOutputs); - traverseForStyle(globalStyles, regionOutputs); + traverseForStyle(timeUs, globalStyles, regionOutputs); List cues = new ArrayList<>(); for (Entry entry : regionOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); - cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, - region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); + cues.add( + new Cue( + cleanUpText(entry.getValue()), + /* textAlignment= */ null, + region.line, + region.lineType, + region.lineAnchor, + region.position, + /* positionAnchor= */ Cue.TYPE_UNSET, + region.width, + region.textSizeType, + region.textSize)); } return cues; } - private void traverseForText(long timeUs, boolean descendsPNode, - String inheritedRegion, Map regionOutputs) { + private void traverseForText( + long timeUs, + boolean descendsPNode, + String inheritedRegion, + Map regionOutputs) { nodeStartsByRegion.clear(); nodeEndsByRegion.clear(); - String resolvedRegionId = regionId; - if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) { - resolvedRegionId = inheritedRegion; + if (TAG_METADATA.equals(tag)) { + // Ignore metadata tag. + return; } + + String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; + if (isTextNode && descendsPNode) { getRegionOutput(resolvedRegionId, regionOutputs).append(text); } else if (TAG_BR.equals(tag) && descendsPNode) { getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); - } else if (TAG_METADATA.equals(tag)) { - // Do nothing. } else if (isActive(timeUs)) { - boolean isPNode = TAG_P.equals(tag); + // This is a container node, which can contain zero or more children. for (Entry entry : regionOutputs.entrySet()) { nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); } + + boolean isPNode = TAG_P.equals(tag); for (int i = 0; i < getChildCount(); i++) { getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId, regionOutputs); @@ -211,39 +227,50 @@ import java.util.TreeSet; if (isPNode) { TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); } + for (Entry entry : regionOutputs.entrySet()) { nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); } } } - private static SpannableStringBuilder getRegionOutput(String resolvedRegionId, - Map regionOutputs) { + private static SpannableStringBuilder getRegionOutput( + String resolvedRegionId, Map regionOutputs) { if (!regionOutputs.containsKey(resolvedRegionId)) { regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); } return regionOutputs.get(resolvedRegionId); } - private void traverseForStyle(Map globalStyles, + private void traverseForStyle( + long timeUs, + Map globalStyles, Map regionOutputs) { + if (!isActive(timeUs)) { + return; + } for (Entry entry : nodeEndsByRegion.entrySet()) { String regionId = entry.getKey(); int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; - applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue()); - for (int i = 0; i < getChildCount(); ++i) { - getChild(i).traverseForStyle(globalStyles, regionOutputs); + int end = entry.getValue(); + if (start != end) { + SpannableStringBuilder regionOutput = regionOutputs.get(regionId); + applyStyleToOutput(globalStyles, regionOutput, start, end); } } + for (int i = 0; i < getChildCount(); ++i) { + getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs); + } } - private void applyStyleToOutput(Map globalStyles, - SpannableStringBuilder regionOutput, int start, int end) { - if (start != end) { - TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - if (resolvedStyle != null) { - TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); - } + private void applyStyleToOutput( + Map globalStyles, + SpannableStringBuilder regionOutput, + int start, + int end) { + TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); + if (resolvedStyle != null) { + TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java index 98823d7a84..2b1e9cf99a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java @@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue; public final String id; public final float position; public final float line; - @Cue.LineType public final int lineType; - @Cue.AnchorType public final int lineAnchor; + public final @Cue.LineType int lineType; + public final @Cue.AnchorType int lineAnchor; public final float width; + public final @Cue.TextSizeType int textSizeType; + public final float textSize; public TtmlRegion(String id) { - this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + this( + id, + /* position= */ Cue.DIMEN_UNSET, + /* line= */ Cue.DIMEN_UNSET, + /* lineType= */ Cue.TYPE_UNSET, + /* lineAnchor= */ Cue.TYPE_UNSET, + /* width= */ Cue.DIMEN_UNSET, + /* textSizeType= */ Cue.TYPE_UNSET, + /* textSize= */ Cue.DIMEN_UNSET); } - public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, float width) { + public TtmlRegion( + String id, + float position, + float line, + @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, + float width, + int textSizeType, + float textSize) { this.id = id; this.position = position; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; this.width = width; + this.textSizeType = textSizeType; + this.textSize = textSize; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 9a58ac07aa..81eb5dd888 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.trackselection; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; @@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index f2b4c7ed3e..71d2544784 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -20,6 +20,7 @@ import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; @@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } @@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index 2d457750e4..b37c8cad67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import java.util.Arrays; /** An array of {@link TrackSelection}s. */ @@ -64,7 +65,7 @@ public final class TrackSelectionArray { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java index 4a2354e180..ce3230fa43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.IOException; @@ -79,7 +80,7 @@ public interface DataSource { * * @return The {@link Uri} from which data is being read, or null if the source is not open. */ - Uri getUri(); + @Nullable Uri getUri(); /** * Closes the source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index a6b89a334d..ad7a9d0147 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -61,7 +61,7 @@ public final class DataSpec { /** * Body for a POST request, null otherwise. */ - public final byte[] postBody; + public final @Nullable byte[] postBody; /** * The absolute position of the data in the full stream. */ @@ -81,12 +81,12 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * {@link DataSpec} is not intended to be used in conjunction with a cache. */ - @Nullable public final String key; + public final @Nullable String key; /** * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. */ - @Flags public final int flags; + public final @Flags int flags; /** * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. @@ -128,7 +128,8 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) { + public DataSpec( + Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); } @@ -143,7 +144,12 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, @Flags int flags) { this(uri, null, absoluteStreamPosition, position, length, key, flags); } @@ -162,7 +168,7 @@ public final class DataSpec { */ public DataSpec( Uri uri, - byte[] postBody, + @Nullable byte[] postBody, long absoluteStreamPosition, long position, long length, @@ -222,4 +228,13 @@ public final class DataSpec { } } + /** + * Returns a copy of this {@link DataSpec} with the specified Uri. + * + * @param uri The new source {@link Uri}. + * @return The copied {@link DataSpec} with the specified Uri. + */ + public DataSpec withUri(Uri uri) { + return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java index a36ccd11b1..729f7fe179 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; @@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource { } @Override - public Uri getUri() { + public @Nullable Uri getUri() { return upstream.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 045fc25338..023567e7df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource { */ public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; + private static final String TAG = "CacheDataSource"; + /** * Flags controlling the cache's behavior. */ @@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource { try { key = CacheUtil.getKey(dataSpec); uri = dataSpec.uri; - actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); + actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri); flags = dataSpec.flags; readPosition = dataSpec.position; @@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else if (currentDataSpecLengthUnset) { - setNoBytesRemainingAndMaybeStoreLength(); + setBytesRemainingAndMaybeStoreLength(0); } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); openNextSource(false); @@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource { return bytesRead; } catch (IOException e) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { - setNoBytesRemainingAndMaybeStoreLength(); + setBytesRemainingAndMaybeStoreLength(0); return C.RESULT_END_OF_INPUT; } handleBeforeThrow(e); @@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource { currentDataSource = nextDataSource; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; long resolvedLength = nextDataSource.open(nextDataSpec); - - // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. - ContentMetadataMutations mutations = new ContentMetadataMutations(); if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { - bytesRemaining = resolvedLength; - ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); + setBytesRemainingAndMaybeStoreLength(resolvedLength); } - if (isReadingFromUpstream()) { - actualUri = currentDataSource.getUri(); - boolean isRedirected = !uri.equals(actualUri); - if (isRedirected) { - ContentMetadataInternal.setRedirectedUri(mutations, actualUri); - } else { - ContentMetadataInternal.removeRedirectedUri(mutations); - } + // TODO find a way to store length and redirected uri in one metadata mutation. + maybeUpdateActualUriFieldAndRedirectedUriMetadata(); + } + + private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() { + if (!isReadingFromUpstream()) { + return; } - if (isWritingToCache()) { + actualUri = currentDataSource.getUri(); + maybeUpdateRedirectedUriMetadata(); + } + + private void maybeUpdateRedirectedUriMetadata() { + if (!isWritingToCache()) { + return; + } + ContentMetadataMutations mutations = new ContentMetadataMutations(); + boolean isRedirected = !uri.equals(actualUri); + if (isRedirected) { + ContentMetadataInternal.setRedirectedUri(mutations, actualUri); + } else { + ContentMetadataInternal.removeRedirectedUri(mutations); + } + try { cache.applyContentMetadataMutations(key, mutations); + } catch (CacheException e) { + String message = + "Couldn't update redirected URI. " + + "This might cause relative URIs get resolved incorrectly."; + Log.w(TAG, message, e); } } - private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { - bytesRemaining = 0; - if (isWritingToCache()) { - cache.setContentLength(key, readPosition); - } - } - - private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { + private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) { ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); - return redirectedUri == null ? defaultUri : redirectedUri; + return redirectedUri == null ? uri : redirectedUri; } private static boolean isCausedByPositionOutOfRange(IOException e) { @@ -447,6 +458,13 @@ public final class CacheDataSource implements DataSource { return false; } + private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException { + this.bytesRemaining = bytesRemaining; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition + bytesRemaining); + } + } + private boolean isReadingFromUpstream() { return !isReadingFromCache(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 7b0b459dd9..89835f31de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import java.io.DataInputStream; @@ -236,7 +237,7 @@ import java.util.TreeSet; } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 7b5fd2c598..3bcfac5053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream.cache; -import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; @@ -26,7 +25,6 @@ import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec; private static final int FLAG_ENCRYPTED_INDEX = 1; - private static final String TAG = "CachedContentIndex"; - private final HashMap keyToContent; private final SparseArray idToKey; private final AtomicFile atomicFile; @@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec; add(cachedContent); hashCode += cachedContent.headerHashCode(version); } - if (input.readInt() != hashCode) { + int fileHashCode = input.readInt(); + boolean isEOF = input.read() == -1; + if (fileHashCode != hashCode || !isEOF) { return false; } - } catch (FileNotFoundException e) { - return false; } catch (IOException e) { - Log.e(TAG, "Error reading cache content index file.", e); return false; } finally { if (input != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java index b855befe00..aefb0f6852 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java new file mode 100644 index 0000000000..6fe76b9b2c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.opengl.GLES20; +import android.os.Handler; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */ +@TargetApi(17) +public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable { + + /** Secure mode to be used by the EGL surface and context. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + public @interface SecureMode {} + + /** No secure EGL surface and context required. */ + public static final int SECURE_MODE_NONE = 0; + /** Creating a surfaceless, secured EGL context. */ + public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + /** Creating a secure surface backed by a pixel buffer. */ + public static final int SECURE_MODE_PROTECTED_PBUFFER = 2; + + private static final int[] EGL_CONFIG_ATTRIBUTES = + new int[] { + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_DEPTH_SIZE, 0, + EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE, + EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, + EGL14.EGL_NONE + }; + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + /** A runtime exception to be thrown if some EGL operations failed. */ + public static final class GlException extends RuntimeException { + private GlException(String msg) { + super(msg); + } + } + + private final Handler handler; + private final int[] textureIdHolder; + + private @Nullable EGLDisplay display; + private @Nullable EGLContext context; + private @Nullable EGLSurface surface; + private @Nullable SurfaceTexture texture; + + /** + * @param handler The {@link Handler} that will be used to call {@link + * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that + * {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s + * looper. + */ + public EGLSurfaceTexture(Handler handler) { + this.handler = handler; + textureIdHolder = new int[1]; + } + + /** + * Initializes required EGL parameters and creates the {@link SurfaceTexture}. + * + * @param secureMode The {@link SecureMode} to be used for EGL surface. + */ + public void init(@SecureMode int secureMode) { + display = getDefaultDisplay(); + EGLConfig config = chooseEGLConfig(display); + context = createEGLContext(display, config, secureMode); + surface = createEGLSurface(display, config, context, secureMode); + generateTextureIds(textureIdHolder); + texture = new SurfaceTexture(textureIdHolder[0]); + texture.setOnFrameAvailableListener(this); + } + + /** Releases all allocated resources. */ + @SuppressWarnings({"nullness:argument.type.incompatible"}) + public void release() { + handler.removeCallbacks(this); + try { + if (texture != null) { + texture.release(); + GLES20.glDeleteTextures(1, textureIdHolder, 0); + } + } finally { + if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) { + EGL14.eglDestroySurface(display, surface); + } + if (context != null) { + EGL14.eglDestroyContext(display, context); + } + display = null; + context = null; + surface = null; + texture = null; + } + } + + /** + * Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}. + */ + public SurfaceTexture getSurfaceTexture() { + return Assertions.checkNotNull(texture); + } + + // SurfaceTexture.OnFrameAvailableListener + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + handler.post(this); + } + + // Runnable + + @Override + public void run() { + if (texture != null) { + texture.updateTexImage(); + } + } + + private static EGLDisplay getDefaultDisplay() { + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (display == null) { + throw new GlException("eglGetDisplay failed"); + } + + int[] version = new int[2]; + boolean eglInitialized = + EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1); + if (!eglInitialized) { + throw new GlException("eglInitialize failed"); + } + return display; + } + + private static EGLConfig chooseEGLConfig(EGLDisplay display) { + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + boolean success = + EGL14.eglChooseConfig( + display, + EGL_CONFIG_ATTRIBUTES, + /* attrib_listOffset= */ 0, + configs, + /* configsOffset= */ 0, + /* config_size= */ 1, + numConfigs, + /* num_configOffset= */ 0); + if (!success || numConfigs[0] <= 0 || configs[0] == null) { + throw new GlException( + Util.formatInvariant( + /* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s", + success, numConfigs[0], configs[0])); + } + + return configs[0]; + } + + private static EGLContext createEGLContext( + EGLDisplay display, EGLConfig config, @SecureMode int secureMode) { + int[] glAttributes; + if (secureMode == SECURE_MODE_NONE) { + glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + } else { + glAttributes = + new int[] { + EGL14.EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } + EGLContext context = + EGL14.eglCreateContext( + display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); + if (context == null) { + throw new GlException("eglCreateContext failed"); + } + return context; + } + + private static EGLSurface createEGLSurface( + EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) { + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL14.EGL_NO_SURFACE; + } else { + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, + 1, + EGL14.EGL_HEIGHT, + 1, + EGL_PROTECTED_CONTENT_EXT, + EGL14.EGL_TRUE, + EGL14.EGL_NONE + }; + } else { + pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, 1, + EGL14.EGL_HEIGHT, 1, + EGL14.EGL_NONE + }; + } + surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0); + if (surface == null) { + throw new GlException("eglCreatePbufferSurface failed"); + } + } + + boolean eglMadeCurrent = + EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context); + if (!eglMadeCurrent) { + throw new GlException("eglMakeCurrent failed"); + } + return surface; + } + + private static void generateTextureIds(int[] textureIdHolder) { + GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0); + int errorCode = GLES20.glGetError(); + if (errorCode != GLES20.GL_NO_ERROR) { + throw new GlException("glGenTextures failed. Error: " + Integer.toHexString(errorCode)); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java index b08f4a31e3..0df39e103d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; + /** * Holder for FLAC stream info. */ @@ -52,8 +54,29 @@ public final class FlacStreamInfo { // Remaining 16 bytes is md5 value } - public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, - int sampleRate, int channels, int bitsPerSample, long totalSamples) { + /** + * Constructs a FlacStreamInfo given the parameters. + * + * @param minBlockSize Minimum block size of the FLAC stream. + * @param maxBlockSize Maximum block size of the FLAC stream. + * @param minFrameSize Minimum frame size of the FLAC stream. + * @param maxFrameSize Maximum frame size of the FLAC stream. + * @param sampleRate Sample rate of the FLAC stream. + * @param channels Number of channels of the FLAC stream. + * @param bitsPerSample Number of bits per sample of the FLAC stream. + * @param totalSamples Total samples of the FLAC stream. + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + */ + public FlacStreamInfo( + int minBlockSize, + int maxBlockSize, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -64,16 +87,43 @@ public final class FlacStreamInfo { this.totalSamples = totalSamples; } + /** Returns the maximum size for a decoded frame from the FLAC stream. */ public int maxDecodedFrameSize() { return maxBlockSize * channels * (bitsPerSample / 8); } + /** Returns the bit-rate of the FLAC stream. */ public int bitRate() { return bitsPerSample * sampleRate; } + /** Returns the duration of the FLAC stream in microseconds. */ public long durationUs() { return (totalSamples * 1000000L) / sampleRate; } + /** + * Returns the sample index for the sample at given position. + * + * @param timeUs Time position in microseconds in the FLAC stream. + * @return The sample index for the sample at given position. + */ + public long getSampleIndex(long timeUs) { + long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND; + return Util.constrainValue(sampleIndex, 0, totalSamples - 1); + } + + /** Returns the approximate number of bytes per frame for the current FLAC stream. */ + public long getApproxBytesPerFrame() { + long approxBytesPerFrame; + if (maxFrameSize > 0) { + approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1; + } else { + // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the + // default value for FLAC block-size, which is 4096. + long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096; + approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64; + } + return approxBytesPerFrame; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java index 6592273d03..071ebf2084 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/UriUtil.java @@ -143,6 +143,26 @@ public final class UriUtil { } } + /** + * Removes query parameter from an Uri, if present. + * + * @param uri The uri. + * @param queryParameterName The name of the query parameter. + * @return The uri without the query parameter. + */ + public static Uri removeQueryParameter(Uri uri, String queryParameterName) { + Uri.Builder builder = uri.buildUpon(); + builder.clearQuery(); + for (String key : uri.getQueryParameterNames()) { + if (!key.equals(queryParameterName)) { + for (String value : uri.getQueryParameters(key)) { + builder.appendQueryParameter(key, value); + } + } + } + return builder.build(); + } + /** * Removes dot segments from the path of a URI. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index a983a0a6a3..faedaaf273 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.video; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; - import java.util.Arrays; /** @@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable { // Parcelable implementation. @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index fc31a33097..2f41831a5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -15,29 +15,29 @@ */ package com.google.android.exoplayer2.video; +import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE; +import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER; +import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT; + import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; -import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; -import android.opengl.EGLConfig; -import android.opengl.EGLContext; import android.opengl.EGLDisplay; -import android.opengl.EGLSurface; -import android.opengl.GLES20; import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; -import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.EGLSurfaceTexture; +import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; import com.google.android.exoplayer2.util.Util; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import javax.microedition.khronos.egl.EGL10; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A dummy {@link Surface}. @@ -50,16 +50,6 @@ public final class DummySurface extends Surface { private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; - private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) - private @interface SecureMode {} - - private static final int SECURE_MODE_NONE = 0; - private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; - private static final int SECURE_MODE_PROTECTED_PBUFFER = 2; - /** * Whether the surface is secure. */ @@ -161,32 +151,25 @@ public final class DummySurface extends Surface { : SECURE_MODE_PROTECTED_PBUFFER; } - private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, - Callback { + private static class DummySurfaceThread extends HandlerThread implements Callback { private static final int MSG_INIT = 1; - private static final int MSG_UPDATE_TEXTURE = 2; - private static final int MSG_RELEASE = 3; + private static final int MSG_RELEASE = 2; - private final int[] textureIdHolder; - private EGLDisplay display; - private EGLContext context; - private EGLSurface pbuffer; - private Handler handler; - private SurfaceTexture surfaceTexture; - - private Error initError; - private RuntimeException initException; - private DummySurface surface; + private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure; + private @MonotonicNonNull Handler handler; + private @Nullable Error initError; + private @Nullable RuntimeException initException; + private @Nullable DummySurface surface; public DummySurfaceThread() { super("dummySurface"); - textureIdHolder = new int[1]; } public DummySurface init(@SecureMode int secureMode) { start(); - handler = new Handler(getLooper(), this); + handler = new Handler(getLooper(), /* callback= */ this); + eglSurfaceTexure = new EGLSurfaceTexture(handler); boolean wasInterrupted = false; synchronized (this) { handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); @@ -207,19 +190,15 @@ public final class DummySurface extends Surface { } else if (initError != null) { throw initError; } else { - return surface; + return Assertions.checkNotNull(surface); } } public void release() { + Assertions.checkNotNull(handler); handler.sendEmptyMessage(MSG_RELEASE); } - @Override - public void onFrameAvailable(SurfaceTexture surfaceTexture) { - handler.sendEmptyMessage(MSG_UPDATE_TEXTURE); - } - @Override public boolean handleMessage(Message msg) { switch (msg.what) { @@ -238,9 +217,6 @@ public final class DummySurface extends Surface { } } return true; - case MSG_UPDATE_TEXTURE: - surfaceTexture.updateTexImage(); - return true; case MSG_RELEASE: try { releaseInternal(); @@ -256,103 +232,16 @@ public final class DummySurface extends Surface { } private void initInternal(@SecureMode int secureMode) { - display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - Assertions.checkState(display != null, "eglGetDisplay failed"); - - int[] version = new int[2]; - boolean eglInitialized = EGL14.eglInitialize(display, version, 0, version, 1); - Assertions.checkState(eglInitialized, "eglInitialize failed"); - - int[] eglAttributes = - new int[] { - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_DEPTH_SIZE, 0, - EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE, - EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, - EGL14.EGL_NONE - }; - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - boolean eglChooseConfigSuccess = - EGL14.eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, numConfigs, 0); - Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null, - "eglChooseConfig failed"); - - EGLConfig config = configs[0]; - int[] glAttributes; - if (secureMode == SECURE_MODE_NONE) { - glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; - } else { - glAttributes = - new int[] { - EGL14.EGL_CONTEXT_CLIENT_VERSION, - 2, - EGL_PROTECTED_CONTENT_EXT, - EGL14.EGL_TRUE, - EGL14.EGL_NONE - }; - } - context = - EGL14.eglCreateContext( - display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); - Assertions.checkState(context != null, "eglCreateContext failed"); - - EGLSurface surface; - if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { - surface = EGL14.EGL_NO_SURFACE; - } else { - int[] pbufferAttributes; - if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { - pbufferAttributes = - new int[] { - EGL14.EGL_WIDTH, - 1, - EGL14.EGL_HEIGHT, - 1, - EGL_PROTECTED_CONTENT_EXT, - EGL14.EGL_TRUE, - EGL14.EGL_NONE - }; - } else { - pbufferAttributes = new int[] {EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE}; - } - pbuffer = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); - surface = pbuffer; - } - - boolean eglMadeCurrent = EGL14.eglMakeCurrent(display, surface, surface, context); - Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); - - GLES20.glGenTextures(1, textureIdHolder, 0); - surfaceTexture = new SurfaceTexture(textureIdHolder[0]); - surfaceTexture.setOnFrameAvailableListener(this); - this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE); + Assertions.checkNotNull(eglSurfaceTexure); + eglSurfaceTexure.init(secureMode); + this.surface = + new DummySurface( + this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); } private void releaseInternal() { - try { - if (surfaceTexture != null) { - surfaceTexture.release(); - GLES20.glDeleteTextures(1, textureIdHolder, 0); - } - } finally { - if (pbuffer != null) { - EGL14.eglDestroySurface(display, pbuffer); - } - if (context != null) { - EGL14.eglDestroyContext(display, context); - } - pbuffer = null; - context = null; - display = null; - surface = null; - surfaceTexture = null; - } + Assertions.checkNotNull(eglSurfaceTexure); + eglSurfaceTexure.release(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 34a3eb7284..579f7c45f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/4006, // https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4104. + // https://github.com/google/ExoPlayer/issues/4134. return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "mido".equals(Util.DEVICE) // Redmi Note 4 @@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || "F3311".equals(Util.DEVICE) // Sony Xperia E5 || "M5c".equals(Util.DEVICE) // Meizu M5C || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E - || "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note + || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note + || "woods_f".equals(Util.MODEL)) // Moto E (4) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index ed91f6651c..0df854cddb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -51,6 +51,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -1812,6 +1813,88 @@ public final class ExoPlayerTest { assertThat(target3.windowIndex).isEqualTo(2); } + @Test + public void testCancelMessageBeforeDelivery() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + final AtomicReference message = new AtomicReference<>(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testCancelMessage") + .pause() + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + message.set( + player.createMessage(target).setPosition(/* positionMs= */ 50).send()); + } + }) + // Play a bit to ensure message arrived in internal player. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 30) + .executeRunnable( + new Runnable() { + @Override + public void run() { + message.get().cancel(); + } + }) + .play() + .build(); + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertThat(message.get().isCanceled()).isTrue(); + assertThat(target.messageCount).isEqualTo(0); + } + + @Test + public void testCancelRepeatedMessageAfterDelivery() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + final AtomicReference message = new AtomicReference<>(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testCancelMessage") + .pause() + .waitForPlaybackState(Player.STATE_BUFFERING) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + message.set( + player + .createMessage(target) + .setPosition(/* positionMs= */ 50) + .setDeleteAfterDelivery(/* deleteAfterDelivery= */ false) + .send()); + } + }) + // Play until the message has been delivered. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 51) + // Seek back, cancel the message, and play past the same position again. + .seek(/* positionMs= */ 0) + .executeRunnable( + new Runnable() { + @Override + public void run() { + message.get().cancel(); + } + }) + .play() + .build(); + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertThat(message.get().isCanceled()).isTrue(); + assertThat(target.messageCount).isEqualTo(1); + } + @Test public void testSetAndSwitchSurface() throws Exception { final List rendererMessages = new ArrayList<>(); @@ -1934,8 +2017,10 @@ public final class ExoPlayerTest { @Override public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { - windowIndex = player.getCurrentWindowIndex(); - positionMs = player.getCurrentPosition(); + if (player != null) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + } messageCount++; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 829fa5a2b8..623506ad0d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (!(other instanceof EventWindowAndPeriodId)) { return false; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java index a52867e1b2..82c62ecb3e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.util; +import static com.google.android.exoplayer2.util.UriUtil.removeQueryParameter; import static com.google.android.exoplayer2.util.UriUtil.resolve; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -104,4 +106,36 @@ public final class UriUtilTest { assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); } + @Test + public void removeOnlyQueryParameter() { + Uri uri = Uri.parse("http://uri?query=value"); + assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri"); + } + + @Test + public void removeFirstQueryParameter() { + Uri uri = Uri.parse("http://uri?query=value&second=value2"); + assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?second=value2"); + } + + @Test + public void removeMiddleQueryParameter() { + Uri uri = Uri.parse("http://uri?first=value1&query=value&last=value2"); + assertThat(removeQueryParameter(uri, "query").toString()) + .isEqualTo("http://uri?first=value1&last=value2"); + } + + @Test + public void removeLastQueryParameter() { + Uri uri = Uri.parse("http://uri?first=value1&query=value"); + assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?first=value1"); + } + + @Test + public void removeNonExistentQueryParameter() { + Uri uri = Uri.parse("http://uri"); + assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri"); + uri = Uri.parse("http://uri?query=value"); + assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri?query=value"); + } } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 81b247d047..867b288498 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -30,15 +30,11 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation project(modulePrefix + 'library-core') - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index 8a6069e477..bd19ff6587 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper { manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); } + /** Returns the DASH manifest. Must not be called until after preparation completes. */ + public DashManifest getManifest() { + Assertions.checkNotNull(manifest); + return manifest; + } + @Override public int getPeriodCount() { Assertions.checkNotNull(manifest); diff --git a/library/hls/build.gradle b/library/hls/build.gradle index c599931a68..6aeb33e195 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -30,15 +30,11 @@ android { // testCoverageEnabled = true // } } - - lintOptions { - lintConfig file("../../checker-framework-lint.xml") - } } dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 9a02bd785a..0fb1b6a969 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -198,24 +198,24 @@ import java.util.List; /** * Returns the next chunk to load. - *

- * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has - * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but - * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to + * + *

If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream + * has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available + * but the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to * contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * * @param previous The most recently loaded media chunk. - * @param playbackPositionUs The current playback position in microseconds. If playback of the - * period to which this chunk source belongs has not yet started, the value will be the - * starting position in the period minus the duration of any media in previous periods still - * to be played. - * @param loadPositionUs The current load position in microseconds. If {@code previous} is null, - * this is the starting position from which chunks should be provided. Else it's equal to - * {@code previous.endTimeUs}. + * @param playbackPositionUs The current playback position relative to the period start in + * microseconds. If playback of the period to which this chunk source belongs has not yet + * started, the value will be the starting position in the period minus the duration of any + * media in previous periods still to be played. + * @param loadPositionUs The current load position relative to the period start in microseconds. + * If {@code previous} is null, this is the starting position from which chunks should be + * provided. Else it's equal to {@code previous.endTimeUs}. * @param out A holder to populate. */ - public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, - HlsChunkHolder out) { + public void getNextChunk( + HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); long bufferedDurationUs = loadPositionUs - playbackPositionUs; @@ -261,12 +261,13 @@ import java.util.List; // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - // The playlist start time is subtracted from the target position because the segment start - // times are relative to the start of the playlist, but the target position is not. + long positionOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs; chunkMediaSequence = Util.binarySearchFloor( mediaPlaylist.segments, - /* value= */ targetPositionUs - mediaPlaylist.startTimeUs, + /* value= */ targetPositionInPlaylistUs, /* inclusive= */ true, /* stayInBounds= */ !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence; @@ -330,9 +331,9 @@ import java.util.List; } // Compute start time of the next chunk. - long offsetFromInitialStartTimeUs = + long positionOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs; + long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs; int discontinuitySequence = mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( @@ -352,8 +353,8 @@ import java.util.List; muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - startTimeUs, - startTimeUs + segment.durationUs, + segmentStartTimeInPeriodUs, + segmentStartTimeInPeriodUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, segment.hasGapTag, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 5d4d953372..f43d119018 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** @@ -36,6 +37,11 @@ import java.io.IOException; sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING; } + public void bindSampleQueue() { + Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING); + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); + } + public void unbindSampleQueue() { if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); @@ -48,12 +54,11 @@ import java.io.IOException; @Override public boolean isReady() { return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL - || (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex)); + || (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex)); } @Override public void maybeThrowError() throws IOException { - maybeMapToSampleQueue(); if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); @@ -63,22 +68,21 @@ import java.io.IOException; @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - return maybeMapToSampleQueue() + return hasValidSampleQueueIndex() ? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat) : C.RESULT_NOTHING_READ; } @Override public int skipData(long positionUs) { - return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0; + return hasValidSampleQueueIndex() + ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) + : 0; } // Internal methods. - private boolean maybeMapToSampleQueue() { - if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { - sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); - } + private boolean hasValidSampleQueueIndex() { return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL && sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 0de4faa9c0..705320bdad 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -102,6 +102,7 @@ import java.util.Arrays; private final Runnable maybeFinishPrepareRunnable; private final Runnable onTracksEndedRunnable; private final Handler handler; + private final ArrayList hlsSampleStreams; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; @@ -166,6 +167,7 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); + hlsSampleStreams = new ArrayList<>(); maybeFinishPrepareRunnable = new Runnable() { @Override @@ -219,9 +221,6 @@ import java.util.Arrays; } public int bindSampleQueueToSampleStream(int trackGroupIndex) { - if (trackGroupToSampleQueueIndex == null) { - return SAMPLE_QUEUE_INDEX_PENDING; - } int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET @@ -295,6 +294,9 @@ import java.util.Arrays; } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; + if (trackGroupToSampleQueueIndex != null) { + ((HlsSampleStream) streams[i]).bindSampleQueue(); + } // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (sampleQueuesBuilt && !seekRequired) { SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; @@ -360,6 +362,7 @@ import java.util.Arrays; } } + updateSampleStreams(streams); seenFirstTrackSelection = true; return seekRequired; } @@ -411,6 +414,7 @@ import java.util.Arrays; loader.release(this); handler.removeCallbacksAndMessages(null); released = true; + hlsSampleStreams.clear(); } @Override @@ -750,6 +754,15 @@ import java.util.Arrays; // Internal methods. + private void updateSampleStreams(SampleStream[] streams) { + hlsSampleStreams.clear(); + for (SampleStream stream : streams) { + if (stream != null) { + hlsSampleStreams.add((HlsSampleStream) stream); + } + } + } + private boolean finishedReadingChunk(HlsMediaChunk chunk) { int chunkUid = chunk.uid; int sampleQueueCount = sampleQueues.length; @@ -807,6 +820,9 @@ import java.util.Arrays; } } } + for (HlsSampleStream sampleStream : hlsSampleStreams) { + sampleStream.bindSampleQueue(); + } } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java index 2d430d2c79..9c9cb532a6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.TrackGroup; import java.io.IOException; @@ -23,7 +24,7 @@ import java.io.IOException; public final class SampleQueueMappingException extends IOException { /** @param mimeType The mime type of the track group whose mapping failed. */ - public SampleQueueMappingException(String mimeType) { + public SampleQueueMappingException(@Nullable String mimeType) { super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index 773aec49ee..37aa181970 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper { playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); } + /** Returns the HLS playlist. Must not be called until after preparation completes. */ + public HlsPlaylist getPlaylist() { + Assertions.checkNotNull(playlist); + return playlist; + } + @Override public int getPeriodCount() { Assertions.checkNotNull(playlist); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index f905def54b..5ac6f37550 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final long startOffsetUs; /** - * The start time of the playlist in playback timebase in microseconds. + * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch. + * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the + * playlist. */ public final long startTimeUs; /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 4ed6aa1656..9986f5b65b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback - * A DefaultTimeBar can be customized by setting attributes, as outlined below. + * + *

A DefaultTimeBar can be customized by setting attributes, as outlined below. * *

Attributes

+ * * The following attributes can be set on a DefaultTimeBar when used in a layout XML file: + * *

+ * *

    *
  • {@code bar_height} - Dimension for the height of the time bar. *
      - *
    • Default: {@link #DEFAULT_BAR_HEIGHT_DP}
    • + *
    • Default: {@link #DEFAULT_BAR_HEIGHT_DP} *
    - *
  • *
  • {@code touch_target_height} - Dimension for the height of the area in which touch * interactions with the time bar are handled. If no height is specified, this also determines * the height of the view. *
      - *
    • Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
    • + *
    • Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP} *
    - *
  • *
  • {@code ad_marker_width} - Dimension for the width of any ad markers shown on the * bar. Ad markers are superimposed on the time bar to show the times at which ads will play. *
      - *
    • Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
    • + *
    • Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP} *
    - *
  • *
  • {@code scrubber_enabled_size} - Dimension for the diameter of the circular scrubber * handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle * should be shown. *
      - *
    • Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
    • + *
    • Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP} *
    - *
  • *
  • {@code scrubber_disabled_size} - Dimension for the diameter of the circular scrubber * handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown. *
      - *
    • Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
    • + *
    • Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP} *
    - *
  • *
  • {@code scrubber_dragged_size} - Dimension for the diameter of the circular scrubber * handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown. *
      - *
    • Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
    • + *
    • Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP} *
    - *
  • *
  • {@code scrubber_drawable} - Optional reference to a drawable to draw for the * scrubber handle. If set, this overrides the default behavior, which is to draw a circle for * the scrubber handle. - *
  • *
  • {@code played_color} - Color for the portion of the time bar representing media * before the current playback position. *
      - *
    • Default: {@link #DEFAULT_PLAYED_COLOR}
    • + *
    • Corresponding method: {@link #setPlayedColor(int)} + *
    • Default: {@link #DEFAULT_PLAYED_COLOR} *
    - *
  • *
  • {@code scrubber_color} - Color for the scrubber handle. *
      - *
    • Default: see {@link #getDefaultScrubberColor(int)}
    • + *
    • Corresponding method: {@link #setScrubberColor(int)} + *
    • Default: see {@link #getDefaultScrubberColor(int)} *
    - *
  • *
  • {@code buffered_color} - Color for the portion of the time bar after the current * played position up to the current buffered position. *
      - *
    • Default: see {@link #getDefaultBufferedColor(int)}
    • + *
    • Corresponding method: {@link #setBufferedColor(int)} + *
    • Default: see {@link #getDefaultBufferedColor(int)} *
    - *
  • *
  • {@code unplayed_color} - Color for the portion of the time bar after the current * buffered position. *
      - *
    • Default: see {@link #getDefaultUnplayedColor(int)}
    • + *
    • Corresponding method: {@link #setUnplayedColor(int)} + *
    • Default: see {@link #getDefaultUnplayedColor(int)} *
    - *
  • *
  • {@code ad_marker_color} - Color for unplayed ad markers. *
      - *
    • Default: {@link #DEFAULT_AD_MARKER_COLOR}
    • + *
    • Corresponding method: {@link #setAdMarkerColor(int)} + *
    • Default: {@link #DEFAULT_AD_MARKER_COLOR} *
    - *
  • *
  • {@code played_ad_marker_color} - Color for played ad markers. *
      - *
    • Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
    • + *
    • Corresponding method: {@link #setPlayedAdMarkerColor(int)} + *
    • Default: see {@link #getDefaultPlayedAdMarkerColor(int)} *
    - *
  • *
*/ public class DefaultTimeBar extends View implements TimeBar { @@ -324,6 +321,72 @@ public class DefaultTimeBar extends View implements TimeBar { } } + /** + * Sets the color for the portion of the time bar representing media before the playback position. + * + * @param playedColor The color for the portion of the time bar representing media before the + * playback position. + */ + public void setPlayedColor(@ColorInt int playedColor) { + playedPaint.setColor(playedColor); + invalidate(seekBounds); + } + + /** + * Sets the color for the scrubber handle. + * + * @param scrubberColor The color for the scrubber handle. + */ + public void setScrubberColor(@ColorInt int scrubberColor) { + scrubberPaint.setColor(scrubberColor); + invalidate(seekBounds); + } + + /** + * Sets the color for the portion of the time bar after the current played position up to the + * current buffered position. + * + * @param bufferedColor The color for the portion of the time bar after the current played + * position up to the current buffered position. + */ + public void setBufferedColor(@ColorInt int bufferedColor) { + bufferedPaint.setColor(bufferedColor); + invalidate(seekBounds); + } + + /** + * Sets the color for the portion of the time bar after the current played position. + * + * @param unplayedColor The color for the portion of the time bar after the current played + * position. + */ + public void setUnplayedColor(@ColorInt int unplayedColor) { + unplayedPaint.setColor(unplayedColor); + invalidate(seekBounds); + } + + /** + * Sets the color for unplayed ad markers. + * + * @param adMarkerColor The color for unplayed ad markers. + */ + public void setAdMarkerColor(@ColorInt int adMarkerColor) { + adMarkerPaint.setColor(adMarkerColor); + invalidate(seekBounds); + } + + /** + * Sets the color for played ad markers. + * + * @param playedAdMarkerColor The color for played ad markers. + */ + public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) { + playedAdMarkerPaint.setColor(playedAdMarkerColor); + invalidate(seekBounds); + } + + // TimeBar implementation. + @Override public void addListener(OnScrubListener listener) { listeners.add(listener); @@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar { update(); } + // View methods. + @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); @@ -408,8 +473,8 @@ public class DefaultTimeBar extends View implements TimeBar { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isInSeekBar(x, y)) { - startScrubbing(); positionScrubber(x); + startScrubbing(); scrubPosition = getScrubberPosition(); update(); invalidate(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 4c258c748f..19051ba932 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -50,6 +50,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A notification manager to start, update and cancel a media style notification reflecting the @@ -205,7 +206,9 @@ public class PlayerNotificationManager { new Runnable() { @Override public void run() { - if (notificationTag == currentNotificationTag && isNotificationStarted) { + if (player != null + && notificationTag == currentNotificationTag + && isNotificationStarted) { updateNotification(bitmap); } } @@ -260,7 +263,7 @@ public class PlayerNotificationManager { private final String channelId; private final int notificationId; private final MediaDescriptionAdapter mediaDescriptionAdapter; - private final CustomActionReceiver customActionReceiver; + private final @Nullable CustomActionReceiver customActionReceiver; private final Handler mainHandler; private final NotificationManagerCompat notificationManager; private final IntentFilter intentFilter; @@ -269,12 +272,12 @@ public class PlayerNotificationManager { private final Map playbackActions; private final Map customActions; - private Player player; + private @Nullable Player player; private ControlDispatcher controlDispatcher; private boolean isNotificationStarted; private int currentNotificationTag; - private NotificationListener notificationListener; - private MediaSessionCompat.Token mediaSessionToken; + private @Nullable NotificationListener notificationListener; + private @Nullable MediaSessionCompat.Token mediaSessionToken; private boolean useNavigationActions; private boolean usePlayPauseActions; private @Nullable String stopAction; @@ -365,6 +368,20 @@ public class PlayerNotificationManager { playerListener = new PlayerListener(); notificationBroadcastReceiver = new NotificationBroadcastReceiver(); intentFilter = new IntentFilter(); + useNavigationActions = true; + usePlayPauseActions = true; + ongoing = true; + colorized = true; + useChronometer = true; + color = Color.TRANSPARENT; + smallIconResourceId = R.drawable.exo_notification_small_icon; + defaults = 0; + priority = NotificationCompat.PRIORITY_LOW; + fastForwardMs = DEFAULT_FAST_FORWARD_MS; + rewindMs = DEFAULT_REWIND_MS; + stopAction = ACTION_STOP; + badgeIconType = NotificationCompat.BADGE_ICON_SMALL; + visibility = NotificationCompat.VISIBILITY_PUBLIC; // initialize actions playbackActions = createPlaybackActions(context); @@ -378,22 +395,7 @@ public class PlayerNotificationManager { for (String action : customActions.keySet()) { intentFilter.addAction(action); } - - setStopAction(ACTION_STOP); - - useNavigationActions = true; - usePlayPauseActions = true; - ongoing = true; - colorized = true; - useChronometer = true; - color = Color.TRANSPARENT; - smallIconResourceId = R.drawable.exo_notification_small_icon; - defaults = 0; - priority = NotificationCompat.PRIORITY_LOW; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; - rewindMs = DEFAULT_REWIND_MS; - setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL); - setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; } /** @@ -512,10 +514,9 @@ public class PlayerNotificationManager { } this.stopAction = stopAction; if (ACTION_STOP.equals(stopAction)) { - stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent; + stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; } else if (stopAction != null) { - Assertions.checkArgument(customActions.containsKey(stopAction)); - stopPendingIntent = customActions.get(stopAction).actionIntent; + stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent; } else { stopPendingIntent = null; } @@ -698,25 +699,28 @@ public class PlayerNotificationManager { maybeUpdateNotification(); } - private Notification updateNotification(Bitmap bitmap) { + @RequiresNonNull("player") + private Notification updateNotification(@Nullable Bitmap bitmap) { Notification notification = createNotification(player, bitmap); notificationManager.notify(notificationId, notification); return notification; } private void startOrUpdateNotification() { - Notification notification = updateNotification(null); - if (!isNotificationStarted) { - isNotificationStarted = true; - context.registerReceiver(notificationBroadcastReceiver, intentFilter); - if (notificationListener != null) { - notificationListener.onNotificationStarted(notificationId, notification); + if (player != null) { + Notification notification = updateNotification(null); + if (!isNotificationStarted) { + isNotificationStarted = true; + context.registerReceiver(notificationBroadcastReceiver, intentFilter); + if (notificationListener != null) { + notificationListener.onNotificationStarted(notificationId, notification); + } } } } private void maybeUpdateNotification() { - if (isNotificationStarted) { + if (isNotificationStarted && player != null) { updateNotification(null); } } @@ -732,64 +736,6 @@ public class PlayerNotificationManager { } } - private Map createPlaybackActions(Context context) { - Map actions = new HashMap<>(); - Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName()); - actions.put( - ACTION_PLAY, - new NotificationCompat.Action( - R.drawable.exo_notification_play, - context.getString(R.string.exo_controls_play_description), - PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName()); - actions.put( - ACTION_PAUSE, - new NotificationCompat.Action( - R.drawable.exo_notification_pause, - context.getString(R.string.exo_controls_pause_description), - PendingIntent.getBroadcast( - context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName()); - actions.put( - ACTION_STOP, - new NotificationCompat.Action( - R.drawable.exo_notification_stop, - context.getString(R.string.exo_controls_stop_description), - PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName()); - actions.put( - ACTION_REWIND, - new NotificationCompat.Action( - R.drawable.exo_notification_rewind, - context.getString(R.string.exo_controls_rewind_description), - PendingIntent.getBroadcast( - context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName()); - actions.put( - ACTION_FAST_FORWARD, - new NotificationCompat.Action( - R.drawable.exo_notification_fastforward, - context.getString(R.string.exo_controls_fastforward_description), - PendingIntent.getBroadcast( - context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName()); - actions.put( - ACTION_PREVIOUS, - new NotificationCompat.Action( - R.drawable.exo_notification_previous, - context.getString(R.string.exo_controls_previous_description), - PendingIntent.getBroadcast( - context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName()); - actions.put( - ACTION_NEXT, - new NotificationCompat.Action( - R.drawable.exo_notification_next, - context.getString(R.string.exo_controls_next_description), - PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT))); - return actions; - } - /** * Creates the notification given the current player state. * @@ -821,7 +767,7 @@ public class PlayerNotificationManager { // Configure stop action (eg. when user dismisses the notification when !isOngoing). boolean useStopAction = stopAction != null && !isPlayingAd; mediaStyle.setShowCancelButton(useStopAction); - if (useStopAction) { + if (useStopAction && stopPendingIntent != null) { builder.setDeleteIntent(stopPendingIntent); mediaStyle.setCancelButtonIntent(stopPendingIntent); } @@ -905,7 +851,7 @@ public class PlayerNotificationManager { if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) { stringActions.add(ACTION_NEXT); } - if (!customActions.isEmpty()) { + if (customActionReceiver != null) { stringActions.addAll(customActionReceiver.getCustomActions(player)); } if (ACTION_STOP.equals(stopAction)) { @@ -932,6 +878,64 @@ public class PlayerNotificationManager { return new int[] {actionIndex}; } + private static Map createPlaybackActions(Context context) { + Map actions = new HashMap<>(); + Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName()); + actions.put( + ACTION_PLAY, + new NotificationCompat.Action( + R.drawable.exo_notification_play, + context.getString(R.string.exo_controls_play_description), + PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName()); + actions.put( + ACTION_PAUSE, + new NotificationCompat.Action( + R.drawable.exo_notification_pause, + context.getString(R.string.exo_controls_pause_description), + PendingIntent.getBroadcast( + context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName()); + actions.put( + ACTION_STOP, + new NotificationCompat.Action( + R.drawable.exo_notification_stop, + context.getString(R.string.exo_controls_stop_description), + PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName()); + actions.put( + ACTION_REWIND, + new NotificationCompat.Action( + R.drawable.exo_notification_rewind, + context.getString(R.string.exo_controls_rewind_description), + PendingIntent.getBroadcast( + context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName()); + actions.put( + ACTION_FAST_FORWARD, + new NotificationCompat.Action( + R.drawable.exo_notification_fastforward, + context.getString(R.string.exo_controls_fastforward_description), + PendingIntent.getBroadcast( + context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName()); + actions.put( + ACTION_PREVIOUS, + new NotificationCompat.Action( + R.drawable.exo_notification_previous, + context.getString(R.string.exo_controls_previous_description), + PendingIntent.getBroadcast( + context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName()); + actions.put( + ACTION_NEXT, + new NotificationCompat.Action( + R.drawable.exo_notification_next, + context.getString(R.string.exo_controls_next_description), + PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT))); + return actions; + } + private class PlayerListener extends Player.DefaultEventListener { @Override @@ -946,7 +950,7 @@ public class PlayerNotificationManager { @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } startOrUpdateNotification(); @@ -954,7 +958,7 @@ public class PlayerNotificationManager { @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } startOrUpdateNotification(); @@ -967,7 +971,7 @@ public class PlayerNotificationManager { @Override public void onRepeatModeChanged(int repeatMode) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } startOrUpdateNotification(); @@ -985,7 +989,8 @@ public class PlayerNotificationManager { @Override public void onReceive(Context context, Intent intent) { - if (!isNotificationStarted) { + Player player = PlayerNotificationManager.this.player; + if (player == null || !isNotificationStarted) { return; } String action = intent.getAction(); @@ -1013,7 +1018,7 @@ public class PlayerNotificationManager { } else if (ACTION_STOP.equals(action)) { controlDispatcher.dispatchStop(player, true); stopNotification(); - } else if (customActions.containsKey(action)) { + } else if (customActionReceiver != null && customActions.containsKey(action)) { customActionReceiver.onCustomAction(player, action, intent); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 25c4318768..a7aa48c0db 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -133,6 +133,12 @@ import java.util.List; *
  • Corresponding method: {@link #setShutterBackgroundColor(int)} *
  • Default: {@code unset} * + *
  • {@code keep_content_on_player_reset} - Whether the currently displayed video frame + * or media artwork is kept visible when the player is reset. + *
      + *
    • Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)} + *
    • Default: {@code false} + *
    *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
      @@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout { private boolean useArtwork; private Bitmap defaultArtwork; private boolean showBuffering; + private boolean keepContentOnPlayerReset; private @Nullable ErrorMessageProvider errorMessageProvider; private @Nullable CharSequence customErrorMessage; private int controllerShowTimeoutMs; @@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout { a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering); + keepContentOnPlayerReset = + a.getBoolean( + R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); controllerHideDuringAds = a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); } finally { @@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout { if (useController) { controller.setPlayer(player); } - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } if (subtitleView != null) { subtitleView.setCues(null); } updateBuffering(); updateErrorMessage(); + updateForCurrentTrackSelections(/* isNewPlayer= */ true); if (player != null) { Player.VideoComponent newVideoComponent = player.getVideoComponent(); if (newVideoComponent != null) { @@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout { } player.addListener(componentListener); maybeShowController(false); - updateForCurrentTrackSelections(); } else { hideController(); - hideArtwork(); } } @@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout { Assertions.checkState(!useArtwork || artworkView != null); if (this.useArtwork != useArtwork) { this.useArtwork = useArtwork; - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(/* isNewPlayer= */ false); } } @@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout { public void setDefaultArtwork(Bitmap defaultArtwork) { if (this.defaultArtwork != defaultArtwork) { this.defaultArtwork = defaultArtwork; - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(/* isNewPlayer= */ false); } } @@ -600,6 +606,32 @@ public class PlayerView extends FrameLayout { } } + /** + * Sets whether the currently displayed video frame or media artwork is kept visible when the + * player is reset. A player reset is defined to mean the player being re-prepared with different + * media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being + * replaced or cleared by calling {@link #setPlayer(Player)}. + * + *

      If enabled, the currently displayed video frame or media artwork will be kept visible until + * the player set on the view has been successfully prepared with new media and loaded enough of + * it to have determined the available tracks. Hence enabling this option allows transitioning + * from playing one piece of media to another, or from using one player instance to another, + * without clearing the view's content. + * + *

      If disabled, the currently displayed video frame or media artwork will be hidden as soon as + * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible. + * Hence the video frame will not be hidden if using a custom layout that omits this view. + * + * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is + * kept visible when the player is reset. + */ + public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) { + if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) { + this.keepContentOnPlayerReset = keepContentOnPlayerReset; + updateForCurrentTrackSelections(/* isNewPlayer= */ false); + } + } + /** * Sets whether a buffering spinner is displayed when the player is in the buffering state. The * buffering spinner is not displayed by default. @@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout { return player != null && player.isPlayingAd() && player.getPlayWhenReady(); } - private void updateForCurrentTrackSelections() { - if (player == null) { + private void updateForCurrentTrackSelections(boolean isNewPlayer) { + if (player == null || player.getCurrentTrackGroups().isEmpty()) { + if (!keepContentOnPlayerReset) { + hideArtwork(); + closeShutter(); + } return; } + + if (isNewPlayer && !keepContentOnPlayerReset) { + // Hide any video from the previous player. + closeShutter(); + } + TrackSelectionArray selections = player.getCurrentTrackSelections(); for (int i = 0; i < selections.length; i++) { if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { @@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout { return; } } + // Video disabled so the shutter must be closed. - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } + closeShutter(); // Display artwork if enabled and available, else hide it. if (useArtwork) { for (int i = 0; i < selections.length; i++) { @@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout { } } + private void closeShutter() { + if (shutterView != null) { + shutterView.setVisibility(View.VISIBLE); + } + } + private void updateBuffering() { if (bufferingView != null) { boolean showBufferingSpinner = @@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout { @Override public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(/* isNewPlayer= */ false); } // Player.EventListener implementation diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index b6cfc9a6f3..c5d264b310 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -372,12 +372,22 @@ import com.google.android.exoplayer2.util.Util; float previousBottom = layout.getLineTop(0); int lineCount = layout.getLineCount(); for (int i = 0; i < lineCount; i++) { - lineBounds.left = layout.getLineLeft(i) - textPaddingX; - lineBounds.right = layout.getLineRight(i) + textPaddingX; + float lineTextBoundLeft = layout.getLineLeft(i); + float lineTextBoundRight = layout.getLineRight(i); + lineBounds.left = lineTextBoundLeft - textPaddingX; + lineBounds.right = lineTextBoundRight + textPaddingX; lineBounds.top = previousBottom; lineBounds.bottom = layout.getLineBottom(i); previousBottom = lineBounds.bottom; - canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint); + float lineTextWidth = lineTextBoundRight - lineTextBoundLeft; + if (lineTextWidth > 0) { + // Do not draw a line's background color if it has no text. + // For some reason, calculating the width manually is more reliable than + // layout.getLineWidth(). + // Sometimes, lineTextBoundRight == lineTextBoundLeft, and layout.getLineWidth() still + // returns non-zero value. + canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint); + } } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index d89f82b7c4..4dbd4d5fec 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput { */ public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; - private static final int FRACTIONAL = 0; - private static final int FRACTIONAL_IGNORE_PADDING = 1; - private static final int ABSOLUTE = 2; - private final List painters; private List cues; - private int textSizeType; + private @Cue.TextSizeType int textSizeType; private float textSize; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; @@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput { public SubtitleView(Context context, AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); - textSizeType = FRACTIONAL; + textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; textSize = DEFAULT_TEXT_SIZE_FRACTION; applyEmbeddedStyles = true; applyEmbeddedFontSizes = true; @@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput { } else { resources = context.getResources(); } - setTextSize(ABSOLUTE, TypedValue.applyDimension(unit, size, resources.getDisplayMetrics())); + setTextSize( + Cue.TEXT_SIZE_TYPE_ABSOLUTE, + TypedValue.applyDimension(unit, size, resources.getDisplayMetrics())); } /** @@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput { * height after the top and bottom padding has been subtracted. */ public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) { - setTextSize(ignorePadding ? FRACTIONAL_IGNORE_PADDING : FRACTIONAL, fractionOfHeight); + setTextSize( + ignorePadding + ? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING + : Cue.TEXT_SIZE_TYPE_FRACTIONAL, + fractionOfHeight); } - private void setTextSize(int textSizeType, float textSize) { + private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { if (this.textSizeType == textSizeType && this.textSize == textSize) { return; } @@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput { // No space to draw subtitles. return; } + int rawViewHeight = rawBottom - rawTop; + int viewHeightMinusPadding = bottom - top; - float textSizePx = textSizeType == ABSOLUTE ? textSize - : textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop)); - if (textSizePx <= 0) { + float defaultViewTextSizePx = + resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding); + if (defaultViewTextSizePx <= 0) { // Text has no height. return; } for (int i = 0; i < cueCount; i++) { - painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style, - textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom); + Cue cue = cues.get(i); + float textSizePx = + resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx); + SubtitlePainter painter = painters.get(i); + painter.draw( + cue, + applyEmbeddedStyles, + applyEmbeddedFontSizes, + style, + textSizePx, + bottomPaddingFraction, + canvas, + left, + top, + right, + bottom); + } + } + + private float resolveTextSizeForCue( + Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) { + if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { + return defaultViewTextSizePx; + } + float defaultCueTextSizePx = + resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); + return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx; + } + + private float resolveTextSize( + @Cue.TextSizeType int textSizeType, + float textSize, + int rawViewHeight, + int viewHeightMinusPadding) { + switch (textSizeType) { + case Cue.TEXT_SIZE_TYPE_ABSOLUTE: + return textSize; + case Cue.TEXT_SIZE_TYPE_FRACTIONAL: + return textSize * viewHeightMinusPadding; + case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING: + return textSize * rawViewHeight; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index 45ccd783e7..be0babf5a8 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -21,6 +21,8 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; +import android.support.annotation.AttrRes; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Pair; import android.view.LayoutInflater; @@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout { private int rendererIndex; private TrackGroupArray trackGroups; private boolean isDisabled; - private SelectionOverride override; + private @Nullable SelectionOverride override; /** * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. @@ -100,11 +102,13 @@ public class TrackSelectionView extends LinearLayout { this(context, null); } - public TrackSelectionView(Context context, AttributeSet attrs) { + public TrackSelectionView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr) { + @SuppressWarnings("nullness") + public TrackSelectionView( + Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray attributeArray = context @@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout { * @param allowAdaptiveSelections Whether adaptive selection is enabled. */ public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) { - if (!this.allowAdaptiveSelections == allowAdaptiveSelections) { + if (this.allowAdaptiveSelections != allowAdaptiveSelections) { this.allowAdaptiveSelections = allowAdaptiveSelections; updateViews(); } @@ -168,12 +172,14 @@ public class TrackSelectionView extends LinearLayout { } /** - * Sets the {@link TrackNameProvider} used to generate the user visible name of each track. + * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and + * updates the view with track names queried from the specified provider. * * @param trackNameProvider The {@link TrackNameProvider} to use. */ public void setTrackNameProvider(TrackNameProvider trackNameProvider) { this.trackNameProvider = Assertions.checkNotNull(trackNameProvider); + updateViews(); } /** @@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout { override = new SelectionOverride(groupIndex, trackIndex); } else { // An existing override is being modified. - boolean isEnabled = ((CheckedTextView) view).isChecked(); int overrideLength = override.length; - if (isEnabled) { + int[] overrideTracks = override.tracks; + if (((CheckedTextView) view).isChecked()) { // Remove the track from the override. if (overrideLength == 1) { // The last track is being removed, so the override becomes empty. override = null; isDisabled = true; } else { - int[] tracks = getTracksRemoving(override.tracks, trackIndex); + int[] tracks = getTracksRemoving(overrideTracks, trackIndex); override = new SelectionOverride(groupIndex, tracks); } } else { - int[] tracks = getTracksAdding(override.tracks, trackIndex); + int[] tracks = getTracksAdding(overrideTracks, trackIndex); override = new SelectionOverride(groupIndex, tracks); } } diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index 3e83396678..6ac92acf9d 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -21,7 +21,7 @@ Video Audio Text - Keiner + Ohne Automatisch Unbekannt %1$d × %2$d @@ -31,5 +31,5 @@ 5.1-Surround-Sound 7.1-Surround-Sound %1$.2f Mbit/s - %1$s und %2$s + %1$s, %2$s diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 9eefc027ed..e127f181e9 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,6 +51,7 @@ + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 2675e1f0d7..de623b59c9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -217,6 +217,11 @@ public class FakeDataSource implements DataSource { return dataSpecs; } + /** Returns whether the data source is currently opened. */ + public final boolean isOpened() { + return opened; + } + protected void onDataRead(int bytesRead) throws IOException { // Do nothing. Can be overridden. } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 905adae092..ffc877bf42 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -159,6 +159,11 @@ public class FakeMediaSource extends BaseMediaSource { } } + /** Asserts that the source has been prepared. */ + public void assertPrepared() { + assertThat(preparedSource).isTrue(); + } + /** * Assert that the source and all periods have been released. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index 639cb82c2d..6432842df4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -26,6 +26,8 @@ import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * A fake {@link TrackOutput}. @@ -114,6 +116,26 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { sampleEndOffsets.get(index)); } + public long getSampleTimeUs(int index) { + return sampleTimesUs.get(index); + } + + public int getSampleFlags(int index) { + return sampleFlags.get(index); + } + + public CryptoData getSampleCryptoData(int index) { + return cryptoDatas.get(index); + } + + public int getSampleCount() { + return sampleTimesUs.size(); + } + + public List getSampleTimesUs() { + return Collections.unmodifiableList(sampleTimesUs); + } + public void assertEquals(FakeTrackOutput expected) { assertThat(format).isEqualTo(expected.format); assertThat(sampleTimesUs).hasSize(expected.sampleTimesUs.size());