mirror of
https://github.com/androidx/media.git
synced 2025-05-06 23:20:42 +08:00
commit
2b55c91af0
@ -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:
|
||||
|
@ -1,19 +0,0 @@
|
||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore path="**/checker-qual-*.jar"/>
|
||||
</issue>
|
||||
</lint>
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.8.0'
|
||||
releaseVersionCode = 2800
|
||||
releaseVersion = '2.8.1'
|
||||
releaseVersionCode = 2801
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
@ -33,6 +33,7 @@ project.ext {
|
||||
robolectricVersion = '3.7.1'
|
||||
autoValueVersion = '1.6'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
testRunnerVersion = '1.0.2'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
@ -18,6 +18,7 @@
|
||||
package="com.google.android.exoplayer2.demo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.SparseIntArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
@ -110,7 +111,7 @@ import java.util.Map;
|
||||
// equals and hashCode implementations.
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (!(other instanceof CastTimeline)) {
|
||||
|
@ -70,7 +70,8 @@ COMMON_OPTIONS="\
|
||||
--enable-decoder=flac \
|
||||
" && \
|
||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||
git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \
|
||||
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
|
||||
cd ffmpeg && \
|
||||
./configure \
|
||||
--libdir=android-libs/armeabi-v7a \
|
||||
--arch=arm \
|
||||
|
BIN
extensions/flac/src/androidTest/assets/bear_no_seek.flac
Normal file
BIN
extensions/flac/src/androidTest/assets/bear_no_seek.flac
Normal file
Binary file not shown.
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
|
||||
public final class FlacExtractorSeekTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
|
||||
private static final int DURATION_US = 2_741_000;
|
||||
private static final Uri FILE_URI = Uri.parse("file:///android_asset/" + NO_SEEKTABLE_FLAC);
|
||||
private static final Random RANDOM = new Random(1234L);
|
||||
|
||||
private FakeExtractorOutput expectedOutput;
|
||||
private FakeTrackOutput expectedTrackOutput;
|
||||
|
||||
private DefaultDataSource dataSource;
|
||||
private PositionHolder positionHolder;
|
||||
private long totalInputLength;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
expectedOutput = new FakeExtractorOutput();
|
||||
extractAllSamplesFromFileToExpectedOutput(getInstrumentation().getContext(), NO_SEEKTABLE_FLAC);
|
||||
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
|
||||
|
||||
dataSource =
|
||||
new DefaultDataSourceFactory(getInstrumentation().getContext(), "UserAgent")
|
||||
.createDataSource();
|
||||
totalInputLength = readInputLength();
|
||||
positionHolder = new PositionHolder();
|
||||
}
|
||||
|
||||
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput());
|
||||
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long targetSeekTimeUs = 987_000;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
||||
|
||||
long targetSeekTimeUs = 0;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
||||
|
||||
long targetSeekTimeUs = 1_234_000;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long numSeek = 100;
|
||||
for (long i = 0; i < numSeek; i++) {
|
||||
long targetSeekTimeUs = RANDOM.nextInt(DURATION_US + 1);
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private long readInputLength() throws IOException {
|
||||
DataSpec dataSpec = new DataSpec(FILE_URI, 0, C.LENGTH_UNSET, null);
|
||||
long totalInputLength = dataSource.open(dataSpec);
|
||||
Util.closeQuietly(dataSource);
|
||||
return totalInputLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the given seek time and keeps reading from input until we can extract at least one
|
||||
* frame from the seek position, or until end-of-input is reached.
|
||||
*
|
||||
* @return The index of the first extracted frame written to the given {@code trackOutput} after
|
||||
* the seek is completed, or -1 if the seek is completed without any extracted frame.
|
||||
*/
|
||||
private int seekToTimeUs(
|
||||
FlacExtractor flacExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput)
|
||||
throws IOException, InterruptedException {
|
||||
int numSampleBeforeSeek = trackOutput.getSampleCount();
|
||||
SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
|
||||
|
||||
long initialSeekLoadPosition = seekPoints.first.position;
|
||||
flacExtractor.seek(initialSeekLoadPosition, seekTimeUs);
|
||||
|
||||
positionHolder.position = C.POSITION_UNSET;
|
||||
ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition);
|
||||
int extractorReadResult = Extractor.RESULT_CONTINUE;
|
||||
while (true) {
|
||||
try {
|
||||
// Keep reading until we can read at least one frame after seek
|
||||
while (extractorReadResult == Extractor.RESULT_CONTINUE
|
||||
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
|
||||
extractorReadResult = flacExtractor.read(extractorInput, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
|
||||
if (extractorReadResult == Extractor.RESULT_SEEK) {
|
||||
extractorInput = getExtractorInputFromPosition(positionHolder.position);
|
||||
extractorReadResult = Extractor.RESULT_CONTINUE;
|
||||
} else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {
|
||||
return -1;
|
||||
} else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
|
||||
// First index after seek = num sample before seek.
|
||||
return numSampleBeforeSeek;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
|
||||
throws IOException, InterruptedException {
|
||||
try {
|
||||
ExtractorInput input = getExtractorInputFromPosition(0);
|
||||
extractor.init(output);
|
||||
while (output.seekMap == null) {
|
||||
extractor.read(input, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
return output.seekMap;
|
||||
}
|
||||
|
||||
private void assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {
|
||||
int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs);
|
||||
// Assert that after seeking, the first sample frame written to output contains the sample
|
||||
// at seek time.
|
||||
trackOutput.assertSample(
|
||||
firstFrameIndexAfterSeek,
|
||||
expectedTrackOutput.getSampleData(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleFlags(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));
|
||||
}
|
||||
|
||||
private int findTargetFrameInExpectedOutput(long seekTimeUs) {
|
||||
List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();
|
||||
for (int i = 0; i < sampleTimes.size() - 1; i++) {
|
||||
long currentSampleTime = sampleTimes.get(i);
|
||||
long nextSampleTime = sampleTimes.get(i + 1);
|
||||
if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return sampleTimes.size() - 1;
|
||||
}
|
||||
|
||||
private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {
|
||||
DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null);
|
||||
dataSource.open(dataSpec);
|
||||
return new DefaultExtractorInput(dataSource, position, totalInputLength);
|
||||
}
|
||||
|
||||
private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)
|
||||
throws IOException, InterruptedException {
|
||||
byte[] data = TestUtil.getByteArray(context, fileName);
|
||||
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
extractor.init(expectedOutput);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||
|
||||
while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {}
|
||||
}
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A {@link SeekMap} implementation for FLAC stream using binary search.
|
||||
*
|
||||
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
|
||||
* frame that contains the target sample.
|
||||
*/
|
||||
/* package */ final class FlacBinarySearchSeeker {
|
||||
|
||||
/**
|
||||
* When seeking within the source, if the offset is smaller than or equal to this value, the seek
|
||||
* operation will be performed using a skip operation. Otherwise, the source will be reloaded at
|
||||
* the new seek position.
|
||||
*/
|
||||
private static final long MAX_SKIP_BYTES = 256 * 1024;
|
||||
|
||||
private final FlacStreamInfo streamInfo;
|
||||
private final FlacBinarySearchSeekMap seekMap;
|
||||
private final FlacDecoderJni decoderJni;
|
||||
|
||||
private final long firstFramePosition;
|
||||
private final long inputLength;
|
||||
private final long approxBytesPerFrame;
|
||||
|
||||
private @Nullable SeekOperationParams pendingSeekOperationParams;
|
||||
|
||||
public FlacBinarySearchSeeker(
|
||||
FlacStreamInfo streamInfo,
|
||||
long firstFramePosition,
|
||||
long inputLength,
|
||||
FlacDecoderJni decoderJni) {
|
||||
this.streamInfo = Assertions.checkNotNull(streamInfo);
|
||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||
this.firstFramePosition = firstFramePosition;
|
||||
this.inputLength = inputLength;
|
||||
this.approxBytesPerFrame = streamInfo.getApproxBytesPerFrame();
|
||||
|
||||
pendingSeekOperationParams = null;
|
||||
seekMap =
|
||||
new FlacBinarySearchSeekMap(
|
||||
streamInfo,
|
||||
firstFramePosition,
|
||||
inputLength,
|
||||
streamInfo.durationUs(),
|
||||
approxBytesPerFrame);
|
||||
}
|
||||
|
||||
/** Returns the seek map for the wrapped FLAC stream. */
|
||||
public SeekMap getSeekMap() {
|
||||
return seekMap;
|
||||
}
|
||||
|
||||
/** Sets the target time in microseconds within the stream to seek to. */
|
||||
public void setSeekTargetUs(long timeUs) {
|
||||
if (pendingSeekOperationParams != null && pendingSeekOperationParams.seekTimeUs == timeUs) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingSeekOperationParams =
|
||||
new SeekOperationParams(
|
||||
timeUs,
|
||||
streamInfo.getSampleIndex(timeUs),
|
||||
/* floorSample= */ 0,
|
||||
/* ceilingSample= */ streamInfo.totalSamples,
|
||||
/* floorPosition= */ firstFramePosition,
|
||||
/* ceilingPosition= */ inputLength,
|
||||
approxBytesPerFrame);
|
||||
}
|
||||
|
||||
/** Returns whether the last operation set by {@link #setSeekTargetUs(long)} is still pending. */
|
||||
public boolean hasPendingSeek() {
|
||||
return pendingSeekOperationParams != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues to handle the pending seek operation. Returns one of the {@code RESULT_} values from
|
||||
* {@link Extractor}.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which data should be read.
|
||||
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
|
||||
* to hold the position of the required seek.
|
||||
* @param outputBuffer If {@link Extractor#RESULT_CONTINUE} is returned, this byte buffer maybe
|
||||
* updated to hold the extracted frame that contains the target sample. The caller needs to
|
||||
* check the byte buffer limit to see if an extracted frame is available.
|
||||
* @return One of the {@code RESULT_} values defined in {@link Extractor}.
|
||||
* @throws IOException If an error occurred reading from the input.
|
||||
* @throws InterruptedException If the thread was interrupted.
|
||||
*/
|
||||
public int handlePendingSeek(
|
||||
ExtractorInput input, PositionHolder seekPositionHolder, ByteBuffer outputBuffer)
|
||||
throws InterruptedException, IOException {
|
||||
outputBuffer.position(0);
|
||||
outputBuffer.limit(0);
|
||||
while (true) {
|
||||
long floorPosition = pendingSeekOperationParams.floorPosition;
|
||||
long ceilingPosition = pendingSeekOperationParams.ceilingPosition;
|
||||
long searchPosition = pendingSeekOperationParams.nextSearchPosition;
|
||||
|
||||
// streamInfo may not contain minFrameSize, in which case this value will be 0.
|
||||
int minFrameSize = Math.max(1, streamInfo.minFrameSize);
|
||||
if (floorPosition + minFrameSize >= ceilingPosition) {
|
||||
// The seeking range is too small for more than 1 frame, so we can just continue from
|
||||
// the floor position.
|
||||
pendingSeekOperationParams = null;
|
||||
decoderJni.reset(floorPosition);
|
||||
return seekToPosition(input, floorPosition, seekPositionHolder);
|
||||
}
|
||||
|
||||
if (!skipInputUntilPosition(input, searchPosition)) {
|
||||
return seekToPosition(input, searchPosition, seekPositionHolder);
|
||||
}
|
||||
|
||||
decoderJni.reset(searchPosition);
|
||||
try {
|
||||
decoderJni.decodeSampleWithBacktrackPosition(
|
||||
outputBuffer, /* retryPosition= */ searchPosition);
|
||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||
// For some reasons, the extractor can't find a frame mid-stream.
|
||||
// Stop the seeking and let it re-try playing at the last search position.
|
||||
pendingSeekOperationParams = null;
|
||||
throw new IOException("Cannot read frame at position " + searchPosition, e);
|
||||
}
|
||||
if (outputBuffer.limit() == 0) {
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();
|
||||
long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex();
|
||||
long nextFrameSamplePosition = decoderJni.getDecodePosition();
|
||||
|
||||
boolean targetSampleInLastFrame =
|
||||
lastFrameSampleIndex <= pendingSeekOperationParams.targetSample
|
||||
&& nextFrameSampleIndex > pendingSeekOperationParams.targetSample;
|
||||
|
||||
if (targetSampleInLastFrame) {
|
||||
pendingSeekOperationParams = null;
|
||||
return Extractor.RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
if (nextFrameSampleIndex <= pendingSeekOperationParams.targetSample) {
|
||||
pendingSeekOperationParams.updateSeekFloor(nextFrameSampleIndex, nextFrameSamplePosition);
|
||||
} else {
|
||||
pendingSeekOperationParams.updateSeekCeiling(lastFrameSampleIndex, searchPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean skipInputUntilPosition(ExtractorInput input, long position)
|
||||
throws IOException, InterruptedException {
|
||||
long bytesToSkip = position - input.getPosition();
|
||||
if (bytesToSkip >= 0 && bytesToSkip <= MAX_SKIP_BYTES) {
|
||||
input.skipFully((int) bytesToSkip);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int seekToPosition(
|
||||
ExtractorInput input, long position, PositionHolder seekPositionHolder) {
|
||||
if (position == input.getPosition()) {
|
||||
return Extractor.RESULT_CONTINUE;
|
||||
} else {
|
||||
seekPositionHolder.position = position;
|
||||
return Extractor.RESULT_SEEK;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains parameters for a pending seek operation by {@link FlacBinarySearchSeeker}.
|
||||
*
|
||||
* <p>This class holds parameters for a binary-search for the {@code targetSample} in the range
|
||||
* [floorPosition, ceilingPosition).
|
||||
*/
|
||||
private static final class SeekOperationParams {
|
||||
private final long seekTimeUs;
|
||||
private final long targetSample;
|
||||
private final long approxBytesPerFrame;
|
||||
private long floorSample;
|
||||
private long ceilingSample;
|
||||
private long floorPosition;
|
||||
private long ceilingPosition;
|
||||
private long nextSearchPosition;
|
||||
|
||||
private SeekOperationParams(
|
||||
long seekTimeUs,
|
||||
long targetSample,
|
||||
long floorSample,
|
||||
long ceilingSample,
|
||||
long floorPosition,
|
||||
long ceilingPosition,
|
||||
long approxBytesPerFrame) {
|
||||
this.seekTimeUs = seekTimeUs;
|
||||
this.floorSample = floorSample;
|
||||
this.ceilingSample = ceilingSample;
|
||||
this.floorPosition = floorPosition;
|
||||
this.ceilingPosition = ceilingPosition;
|
||||
this.targetSample = targetSample;
|
||||
this.approxBytesPerFrame = approxBytesPerFrame;
|
||||
updateNextSearchPosition();
|
||||
}
|
||||
|
||||
/** Updates the floor constraints (inclusive) of the seek operation. */
|
||||
private void updateSeekFloor(long floorSample, long floorPosition) {
|
||||
this.floorSample = floorSample;
|
||||
this.floorPosition = floorPosition;
|
||||
updateNextSearchPosition();
|
||||
}
|
||||
|
||||
/** Updates the ceiling constraints (exclusive) of the seek operation. */
|
||||
private void updateSeekCeiling(long ceilingSample, long ceilingPosition) {
|
||||
this.ceilingSample = ceilingSample;
|
||||
this.ceilingPosition = ceilingPosition;
|
||||
updateNextSearchPosition();
|
||||
}
|
||||
|
||||
private void updateNextSearchPosition() {
|
||||
this.nextSearchPosition =
|
||||
getNextSearchPosition(
|
||||
targetSample,
|
||||
floorSample,
|
||||
ceilingSample,
|
||||
floorPosition,
|
||||
ceilingPosition,
|
||||
approxBytesPerFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next position in FLAC stream to search for target sample, given [floorPosition,
|
||||
* ceilingPosition).
|
||||
*/
|
||||
private static long getNextSearchPosition(
|
||||
long targetSample,
|
||||
long floorSample,
|
||||
long ceilingSample,
|
||||
long floorPosition,
|
||||
long ceilingPosition,
|
||||
long approxBytesPerFrame) {
|
||||
if (floorPosition + 1 >= ceilingPosition || floorSample + 1 >= ceilingSample) {
|
||||
return floorPosition;
|
||||
}
|
||||
long samplesToSkip = targetSample - floorSample;
|
||||
long estimatedBytesPerSample =
|
||||
Math.max(1, (ceilingPosition - floorPosition) / (ceilingSample - floorSample));
|
||||
// In the stream, the samples are accessed in a group of frame. Given a stream position, the
|
||||
// seeker will be able to find the first frame following that position.
|
||||
// Hence, if our target sample is in the middle of a frame, and our estimate position is
|
||||
// correct, or very near the actual sample position, the seeker will keep accessing the next
|
||||
// frame, rather than the frame that contains the target sample.
|
||||
// Moreover, it's better to under-estimate rather than over-estimate, because the extractor
|
||||
// input can skip forward easily, but cannot rewind easily (it may require a new connection
|
||||
// to be made).
|
||||
// Therefore, we should reduce the estimated position by some amount, so it will converge to
|
||||
// the correct frame earlier.
|
||||
long bytesToSkip = samplesToSkip * estimatedBytesPerSample;
|
||||
long confidenceInterval = bytesToSkip / 20;
|
||||
|
||||
long estimatedFramePosition = floorPosition + bytesToSkip - (approxBytesPerFrame - 1);
|
||||
long estimatedPosition = estimatedFramePosition - confidenceInterval;
|
||||
|
||||
return Util.constrainValue(estimatedPosition, floorPosition, ceilingPosition - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link SeekMap} implementation that returns the estimated byte location from {@link
|
||||
* SeekOperationParams#getNextSearchPosition(long, long, long, long, long, long)} for each {@link
|
||||
* #getSeekPoints(long)} query.
|
||||
*/
|
||||
private static final class FlacBinarySearchSeekMap implements SeekMap {
|
||||
private final FlacStreamInfo streamInfo;
|
||||
private final long firstFramePosition;
|
||||
private final long inputLength;
|
||||
private final long approxBytesPerFrame;
|
||||
private final long durationUs;
|
||||
|
||||
private FlacBinarySearchSeekMap(
|
||||
FlacStreamInfo streamInfo,
|
||||
long firstFramePosition,
|
||||
long inputLength,
|
||||
long durationUs,
|
||||
long approxBytesPerFrame) {
|
||||
this.streamInfo = streamInfo;
|
||||
this.firstFramePosition = firstFramePosition;
|
||||
this.inputLength = inputLength;
|
||||
this.approxBytesPerFrame = approxBytesPerFrame;
|
||||
this.durationUs = durationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekPoints getSeekPoints(long timeUs) {
|
||||
long nextSearchPosition =
|
||||
SeekOperationParams.getNextSearchPosition(
|
||||
streamInfo.getSampleIndex(timeUs),
|
||||
/* floorSample= */ 0,
|
||||
/* ceilingSample= */ streamInfo.totalSamples,
|
||||
/* floorPosition= */ firstFramePosition,
|
||||
/* ceilingPosition= */ inputLength,
|
||||
approxBytesPerFrame);
|
||||
return new SeekPoints(new SeekPoint(timeUs, nextSearchPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return durationUs;
|
||||
}
|
||||
}
|
||||
}
|
@ -92,18 +92,14 @@ import java.util.List;
|
||||
}
|
||||
decoderJni.setData(inputBuffer.data);
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
|
||||
int result;
|
||||
try {
|
||||
result = decoderJni.decodeSample(outputData);
|
||||
decoderJni.decodeSample(outputData);
|
||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||
return new FlacDecoderException("Frame decoding failed", e);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// Never happens.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
if (result < 0) {
|
||||
return new FlacDecoderException("Frame decoding failed");
|
||||
}
|
||||
outputData.position(0);
|
||||
outputData.limit(result);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,17 @@ import java.nio.ByteBuffer;
|
||||
*/
|
||||
/* package */ final class FlacDecoderJni {
|
||||
|
||||
/** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */
|
||||
public static final class FlacFrameDecodeException extends Exception {
|
||||
|
||||
public final int errorCode;
|
||||
|
||||
public FlacFrameDecodeException(String message, int errorCode) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
|
||||
|
||||
private final long nativeDecoderContext;
|
||||
@ -116,14 +127,50 @@ import java.nio.ByteBuffer;
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
|
||||
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
|
||||
return flacDecodeMetadata(nativeDecoderContext);
|
||||
}
|
||||
|
||||
public int decodeSample(ByteBuffer output) throws IOException, InterruptedException {
|
||||
return output.isDirect()
|
||||
? flacDecodeToBuffer(nativeDecoderContext, output)
|
||||
: flacDecodeToArray(nativeDecoderContext, output.array());
|
||||
/**
|
||||
* Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO
|
||||
* error occurs, resets the stream and input to the given {@code retryPosition}.
|
||||
*
|
||||
* @param output The byte buffer to hold the decoded frame.
|
||||
* @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
|
||||
*/
|
||||
public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
|
||||
throws InterruptedException, IOException, FlacFrameDecodeException {
|
||||
try {
|
||||
decodeSample(output);
|
||||
} catch (IOException e) {
|
||||
if (retryPosition >= 0) {
|
||||
reset(retryPosition);
|
||||
if (extractorInput != null) {
|
||||
extractorInput.setRetryPosition(retryPosition, e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
|
||||
public void decodeSample(ByteBuffer output)
|
||||
throws IOException, InterruptedException, FlacFrameDecodeException {
|
||||
output.clear();
|
||||
int frameSize =
|
||||
output.isDirect()
|
||||
? flacDecodeToBuffer(nativeDecoderContext, output)
|
||||
: flacDecodeToArray(nativeDecoderContext, output.array());
|
||||
if (frameSize < 0) {
|
||||
if (!isDecoderAtEndOfInput()) {
|
||||
throw new FlacFrameDecodeException("Cannot decode FLAC frame", frameSize);
|
||||
}
|
||||
// The decoder has read to EOI. Return a 0-size frame to indicate the EOI.
|
||||
output.limit(0);
|
||||
} else {
|
||||
output.limit(frameSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,8 +180,19 @@ import java.nio.ByteBuffer;
|
||||
return flacGetDecodePosition(nativeDecoderContext);
|
||||
}
|
||||
|
||||
public long getLastSampleTimestamp() {
|
||||
return flacGetLastTimestamp(nativeDecoderContext);
|
||||
/** Returns the timestamp for the first sample in the last decoded frame. */
|
||||
public long getLastFrameTimestamp() {
|
||||
return flacGetLastFrameTimestamp(nativeDecoderContext);
|
||||
}
|
||||
|
||||
/** Returns the first sample index of the last extracted frame. */
|
||||
public long getLastFrameFirstSampleIndex() {
|
||||
return flacGetLastFrameFirstSampleIndex(nativeDecoderContext);
|
||||
}
|
||||
|
||||
/** Returns the first sample index of the frame to be extracted next. */
|
||||
public long getNextFrameFirstSampleIndex() {
|
||||
return flacGetNextFrameFirstSampleIndex(nativeDecoderContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,6 +211,11 @@ import java.nio.ByteBuffer;
|
||||
return flacGetStateString(nativeDecoderContext);
|
||||
}
|
||||
|
||||
/** Returns whether the decoder has read to the end of the input. */
|
||||
public boolean isDecoderAtEndOfInput() {
|
||||
return flacIsDecoderAtEndOfStream(nativeDecoderContext);
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
flacFlush(nativeDecoderContext);
|
||||
}
|
||||
@ -181,18 +244,34 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
private native long flacInit();
|
||||
|
||||
private native FlacStreamInfo flacDecodeMetadata(long context)
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
private native int flacDecodeToArray(long context, byte[] outputArray)
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
private native long flacGetDecodePosition(long context);
|
||||
private native long flacGetLastTimestamp(long context);
|
||||
|
||||
private native long flacGetLastFrameTimestamp(long context);
|
||||
|
||||
private native long flacGetLastFrameFirstSampleIndex(long context);
|
||||
|
||||
private native long flacGetNextFrameFirstSampleIndex(long context);
|
||||
|
||||
private native long flacGetSeekPosition(long context, long timeUs);
|
||||
|
||||
private native String flacGetStateString(long context);
|
||||
|
||||
private native boolean flacIsDecoderAtEndOfStream(long context);
|
||||
|
||||
private native void flacFlush(long context);
|
||||
|
||||
private native void flacReset(long context, long newPosition);
|
||||
|
||||
private native void flacRelease(long context);
|
||||
|
||||
}
|
||||
|
@ -88,10 +88,12 @@ public final class FlacExtractor implements Extractor {
|
||||
|
||||
private ParsableByteArray outputBuffer;
|
||||
private ByteBuffer outputByteBuffer;
|
||||
private FlacStreamInfo streamInfo;
|
||||
|
||||
private Metadata id3Metadata;
|
||||
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
|
||||
|
||||
private boolean metadataParsed;
|
||||
private boolean readPastStreamInfo;
|
||||
|
||||
/** Constructs an instance with flags = 0. */
|
||||
public FlacExtractor() {
|
||||
@ -136,83 +138,43 @@ public final class FlacExtractor implements Extractor {
|
||||
}
|
||||
|
||||
decoderJni.setData(input);
|
||||
readPastStreamInfo(input);
|
||||
|
||||
if (!metadataParsed) {
|
||||
final FlacStreamInfo streamInfo;
|
||||
try {
|
||||
streamInfo = decoderJni.decodeMetadata();
|
||||
if (streamInfo == null) {
|
||||
throw new IOException("Metadata decoding failed");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
decoderJni.reset(0);
|
||||
input.setRetryPosition(0, e);
|
||||
throw e; // never executes
|
||||
}
|
||||
metadataParsed = true;
|
||||
|
||||
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
|
||||
extractorOutput.seekMap(
|
||||
isSeekable
|
||||
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
|
||||
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
streamInfo.bitRate(),
|
||||
streamInfo.maxDecodedFrameSize(),
|
||||
streamInfo.channels,
|
||||
streamInfo.sampleRate,
|
||||
getPcmEncoding(streamInfo.bitsPerSample),
|
||||
/* encoderDelay= */ 0,
|
||||
/* encoderPadding= */ 0,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null,
|
||||
isId3MetadataDisabled ? null : id3Metadata);
|
||||
trackOutput.format(mediaFormat);
|
||||
|
||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
|
||||
return handlePendingSeek(input, seekPosition);
|
||||
}
|
||||
|
||||
outputBuffer.reset();
|
||||
long lastDecodePosition = decoderJni.getDecodePosition();
|
||||
int size;
|
||||
try {
|
||||
size = decoderJni.decodeSample(outputByteBuffer);
|
||||
} catch (IOException e) {
|
||||
if (lastDecodePosition >= 0) {
|
||||
decoderJni.reset(lastDecodePosition);
|
||||
input.setRetryPosition(lastDecodePosition, e);
|
||||
}
|
||||
throw e;
|
||||
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
|
||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
|
||||
}
|
||||
if (size <= 0) {
|
||||
int outputSize = outputByteBuffer.limit();
|
||||
if (outputSize == 0) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
trackOutput.sampleData(outputBuffer, size);
|
||||
trackOutput.sampleMetadata(decoderJni.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size,
|
||||
0, null);
|
||||
|
||||
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
|
||||
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
if (position == 0) {
|
||||
metadataParsed = false;
|
||||
readPastStreamInfo = false;
|
||||
}
|
||||
if (decoderJni != null) {
|
||||
decoderJni.reset(position);
|
||||
}
|
||||
if (flacBinarySearchSeeker != null) {
|
||||
flacBinarySearchSeeker.setSeekTargetUs(timeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
flacBinarySearchSeeker = null;
|
||||
if (decoderJni != null) {
|
||||
decoderJni.release();
|
||||
decoderJni = null;
|
||||
@ -244,6 +206,100 @@ public final class FlacExtractor implements Extractor {
|
||||
return Arrays.equals(header, FLAC_SIGNATURE);
|
||||
}
|
||||
|
||||
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
|
||||
if (readPastStreamInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlacStreamInfo streamInfo = decodeStreamInfo(input);
|
||||
readPastStreamInfo = true;
|
||||
if (this.streamInfo == null) {
|
||||
updateFlacStreamInfo(input, streamInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
outputSeekMap(input, streamInfo);
|
||||
outputFormat(streamInfo);
|
||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
||||
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
||||
}
|
||||
|
||||
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
||||
throws InterruptedException, IOException {
|
||||
try {
|
||||
FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
|
||||
if (streamInfo == null) {
|
||||
throw new IOException("Metadata decoding failed");
|
||||
}
|
||||
return streamInfo;
|
||||
} catch (IOException e) {
|
||||
decoderJni.reset(0);
|
||||
input.setRetryPosition(0, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
|
||||
boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
|
||||
SeekMap seekMap =
|
||||
hasSeekTable
|
||||
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
|
||||
: getSeekMapForNonSeekTableFlac(input, streamInfo);
|
||||
extractorOutput.seekMap(seekMap);
|
||||
}
|
||||
|
||||
private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
|
||||
long inputLength = input.getLength();
|
||||
if (inputLength != C.LENGTH_UNSET) {
|
||||
long firstFramePosition = decoderJni.getDecodePosition();
|
||||
flacBinarySearchSeeker =
|
||||
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
|
||||
return flacBinarySearchSeeker.getSeekMap();
|
||||
} else { // can't seek at all, because there's no SeekTable and the input length is unknown.
|
||||
return new SeekMap.Unseekable(streamInfo.durationUs());
|
||||
}
|
||||
}
|
||||
|
||||
private void outputFormat(FlacStreamInfo streamInfo) {
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
streamInfo.bitRate(),
|
||||
streamInfo.maxDecodedFrameSize(),
|
||||
streamInfo.channels,
|
||||
streamInfo.sampleRate,
|
||||
getPcmEncoding(streamInfo.bitsPerSample),
|
||||
/* encoderDelay= */ 0,
|
||||
/* encoderPadding= */ 0,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null,
|
||||
isId3MetadataDisabled ? null : id3Metadata);
|
||||
trackOutput.format(mediaFormat);
|
||||
}
|
||||
|
||||
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws InterruptedException, IOException {
|
||||
int seekResult =
|
||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
|
||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
|
||||
}
|
||||
return seekResult;
|
||||
}
|
||||
|
||||
private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
|
||||
outputBuffer.setPosition(0);
|
||||
trackOutput.sampleData(outputBuffer, size);
|
||||
trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
|
||||
}
|
||||
|
||||
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
|
||||
private static final class FlacSeekMap implements SeekMap {
|
||||
|
||||
private final long durationUs;
|
||||
|
@ -133,9 +133,19 @@ DECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {
|
||||
return context->parser->getDecodePosition();
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, flacGetLastTimestamp, jlong jContext) {
|
||||
DECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {
|
||||
Context *context = reinterpret_cast<Context *>(jContext);
|
||||
return context->parser->getLastTimestamp();
|
||||
return context->parser->getLastFrameTimestamp();
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {
|
||||
Context *context = reinterpret_cast<Context *>(jContext);
|
||||
return context->parser->getLastFrameFirstSampleIndex();
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {
|
||||
Context *context = reinterpret_cast<Context *>(jContext);
|
||||
return context->parser->getNextFrameFirstSampleIndex();
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) {
|
||||
@ -149,6 +159,11 @@ DECODER_FUNC(jstring, flacGetStateString, jlong jContext) {
|
||||
return env->NewStringUTF(str);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {
|
||||
Context *context = reinterpret_cast<Context *>(jContext);
|
||||
return context->parser->isDecoderAtEndOfStream();
|
||||
}
|
||||
|
||||
DECODER_FUNC(void, flacFlush, jlong jContext) {
|
||||
Context *context = reinterpret_cast<Context *>(jContext);
|
||||
context->parser->flush();
|
||||
|
@ -44,10 +44,18 @@ class FLACParser {
|
||||
return mStreamInfo;
|
||||
}
|
||||
|
||||
int64_t getLastTimestamp() const {
|
||||
int64_t getLastFrameTimestamp() const {
|
||||
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
|
||||
}
|
||||
|
||||
int64_t getLastFrameFirstSampleIndex() const {
|
||||
return mWriteHeader.number.sample_number;
|
||||
}
|
||||
|
||||
int64_t getNextFrameFirstSampleIndex() const {
|
||||
return mWriteHeader.number.sample_number + mWriteHeader.blocksize;
|
||||
}
|
||||
|
||||
bool decodeMetadata();
|
||||
size_t readBuffer(void *output, size_t output_size);
|
||||
|
||||
@ -83,6 +91,11 @@ class FLACParser {
|
||||
return FLAC__stream_decoder_get_resolved_state_string(mDecoder);
|
||||
}
|
||||
|
||||
bool isDecoderAtEndOfStream() const {
|
||||
return FLAC__stream_decoder_get_state(mDecoder) ==
|
||||
FLAC__STREAM_DECODER_END_OF_STREAM;
|
||||
}
|
||||
|
||||
private:
|
||||
DataSource *mDataSource;
|
||||
|
||||
|
@ -649,18 +649,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
|
||||
@Override
|
||||
public void loadAd(String adUriString) {
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Unexpected loadAd without LOADED event; assuming ad group index is actually "
|
||||
+ expectedAdGroupIndex);
|
||||
adGroupIndex = expectedAdGroupIndex;
|
||||
adsManager.start();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
|
||||
}
|
||||
try {
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Unexpected loadAd without LOADED event; assuming ad group index is actually "
|
||||
+ expectedAdGroupIndex);
|
||||
adGroupIndex = expectedAdGroupIndex;
|
||||
adsManager.start();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
|
||||
}
|
||||
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
|
||||
if (adIndexInAdGroup == C.INDEX_UNSET) {
|
||||
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
|
||||
|
@ -170,7 +170,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||
|
||||
// Check for a valid response code.
|
||||
if (!response.isSuccessful()) {
|
||||
Map<String, List<String>> headers = request.headers().toMultimap();
|
||||
Map<String, List<String>> headers = response.headers().toMultimap();
|
||||
closeConnectionQuietly();
|
||||
InvalidResponseCodeException exception = new InvalidResponseCodeException(
|
||||
responseCode, headers, dataSpec);
|
||||
|
@ -22,6 +22,13 @@ android {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
// Workaround to prevent circular dependency on project :testutils.
|
||||
@ -42,19 +49,17 @@ android {
|
||||
// testCoverageEnabled = true
|
||||
// }
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("../../checker-framework-lint.xml")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestImplementation 'com.google.truth:truth:' + truthVersion
|
||||
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
androidTestImplementation 'com.android.support.test:runner:' + testRunnerVersion
|
||||
androidTestUtil 'com.android.support.test:orchestrator:' + testRunnerVersion
|
||||
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||
testImplementation 'junit:junit:' + junitVersion
|
||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||
|
@ -16,8 +16,8 @@
|
||||
package com.google.android.exoplayer2.upstream;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static junit.framework.Assert.fail;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
@ -28,48 +28,58 @@ import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ContentDataSource}.
|
||||
*/
|
||||
public final class ContentDataSourceTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link ContentDataSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class ContentDataSourceTest {
|
||||
|
||||
private static final String AUTHORITY = "com.google.android.exoplayer2.core.test";
|
||||
private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3";
|
||||
|
||||
@Test
|
||||
public void testRead() throws Exception {
|
||||
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false);
|
||||
assertData(0, C.LENGTH_UNSET, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPipeMode() throws Exception {
|
||||
assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true);
|
||||
assertData(0, C.LENGTH_UNSET, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFixedLength() throws Exception {
|
||||
assertData(getInstrumentation(), 0, 100, false);
|
||||
assertData(0, 100, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFromOffsetToEndOfInput() throws Exception {
|
||||
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false);
|
||||
assertData(1, C.LENGTH_UNSET, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {
|
||||
assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true);
|
||||
assertData(1, C.LENGTH_UNSET, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFromOffsetFixedLength() throws Exception {
|
||||
assertData(getInstrumentation(), 1, 100, false);
|
||||
assertData(1, 100, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadInvalidUri() throws Exception {
|
||||
ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext());
|
||||
ContentDataSource dataSource =
|
||||
new ContentDataSource(InstrumentationRegistry.getTargetContext());
|
||||
Uri contentUri = TestContentProvider.buildUri("does/not.exist", false);
|
||||
DataSpec dataSpec = new DataSpec(contentUri);
|
||||
try {
|
||||
@ -83,13 +93,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertData(Instrumentation instrumentation, int offset, int length,
|
||||
boolean pipeMode) throws IOException {
|
||||
private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
|
||||
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
|
||||
ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext());
|
||||
ContentDataSource dataSource =
|
||||
new ContentDataSource(InstrumentationRegistry.getTargetContext());
|
||||
try {
|
||||
DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);
|
||||
byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH);
|
||||
byte[] completeData =
|
||||
TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH);
|
||||
byte[] expectedData = Arrays.copyOfRange(completeData, offset,
|
||||
length == C.LENGTH_UNSET ? completeData.length : offset + length);
|
||||
TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);
|
||||
|
@ -19,7 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
@ -29,9 +30,14 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests {@link CachedContentIndex}. */
|
||||
public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class CachedContentIndexTest {
|
||||
|
||||
private final byte[] testIndexV1File = {
|
||||
0, 0, 0, 1, // version
|
||||
@ -70,19 +76,19 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
private CachedContentIndex index;
|
||||
private File cacheDir;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
cacheDir =
|
||||
Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
@After
|
||||
public void tearDown() {
|
||||
Util.recursiveDelete(cacheDir);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddGetRemove() throws Exception {
|
||||
final String key1 = "key1";
|
||||
final String key2 = "key2";
|
||||
@ -132,10 +138,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(cacheSpanFile.exists()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreAndLoad() throws Exception {
|
||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadV1() throws Exception {
|
||||
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
||||
fos.write(testIndexV1File);
|
||||
@ -153,6 +161,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadV2() throws Exception {
|
||||
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
|
||||
fos.write(testIndexV2File);
|
||||
@ -171,7 +180,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560);
|
||||
}
|
||||
|
||||
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
|
||||
@Test
|
||||
public void testAssignIdForKeyAndGetKeyForId() {
|
||||
final String key1 = "key1";
|
||||
final String key2 = "key2";
|
||||
int id1 = index.assignIdForKey(key1);
|
||||
@ -183,7 +193,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
|
||||
}
|
||||
|
||||
public void testGetNewId() throws Exception {
|
||||
@Test
|
||||
public void testGetNewId() {
|
||||
SparseArray<String> idToKey = new SparseArray<>();
|
||||
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
|
||||
idToKey.put(10, "");
|
||||
@ -194,6 +205,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryption() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
@ -250,7 +262,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
|
||||
}
|
||||
|
||||
public void testRemoveEmptyNotLockedCachedContent() throws Exception {
|
||||
@Test
|
||||
public void testRemoveEmptyNotLockedCachedContent() {
|
||||
CachedContent cachedContent = index.getOrAdd("key1");
|
||||
|
||||
index.maybeRemove(cachedContent.key);
|
||||
@ -258,6 +271,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(index.get(cachedContent.key)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCantRemoveNotEmptyCachedContent() throws Exception {
|
||||
CachedContent cachedContent = index.getOrAdd("key1");
|
||||
File cacheSpanFile =
|
||||
@ -270,7 +284,8 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
assertThat(index.get(cachedContent.key)).isNotNull();
|
||||
}
|
||||
|
||||
public void testCantRemoveLockedCachedContent() throws Exception {
|
||||
@Test
|
||||
public void testCantRemoveLockedCachedContent() {
|
||||
CachedContent cachedContent = index.getOrAdd("key1");
|
||||
cachedContent.setLocked(true);
|
||||
|
||||
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.upstream.cache;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -26,11 +27,14 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SimpleCacheSpan}.
|
||||
*/
|
||||
public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
||||
/** Unit tests for {@link SimpleCacheSpan}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SimpleCacheSpanTest {
|
||||
|
||||
private CachedContentIndex index;
|
||||
private File cacheDir;
|
||||
@ -49,19 +53,19 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
||||
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
cacheDir =
|
||||
Util.createTempDirectory(InstrumentationRegistry.getTargetContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
@After
|
||||
public void tearDown() {
|
||||
Util.recursiveDelete(cacheDir);
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheFile() throws Exception {
|
||||
assertCacheSpan("key1", 0, 0);
|
||||
assertCacheSpan("key2", 1, 2);
|
||||
@ -80,6 +84,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
||||
+ "A paragraph-separator character \u2029", 1, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpgradeFileName() throws Exception {
|
||||
String key = "asd\u00aa";
|
||||
int id = index.assignIdForKey(key);
|
||||
|
@ -46,11 +46,10 @@ public class DefaultLoadControl implements LoadControl {
|
||||
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500;
|
||||
|
||||
/**
|
||||
* The default duration of media that must be buffered for playback to resume after a rebuffer,
|
||||
* in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
|
||||
* action.
|
||||
* The default duration of media that must be buffered for playback to resume after a rebuffer, in
|
||||
* milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.
|
||||
*/
|
||||
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
|
||||
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
|
||||
|
||||
/**
|
||||
* The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
|
||||
|
@ -185,10 +185,6 @@ public interface ExoPlayer extends Player {
|
||||
*/
|
||||
Looper getPlaybackLooper();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
ExoPlaybackException getPlaybackError();
|
||||
|
||||
/**
|
||||
* Prepares the player to play the provided {@link MediaSource}. Equivalent to
|
||||
* {@code prepare(mediaSource, true, true)}.
|
||||
|
@ -193,6 +193,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
if (this.playWhenReady != playWhenReady) {
|
||||
this.playWhenReady = playWhenReady;
|
||||
internalPlayer.setPlayWhenReady(playWhenReady);
|
||||
PlaybackInfo playbackInfo = this.playbackInfo;
|
||||
for (Player.EventListener listener : listeners) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
|
||||
}
|
||||
@ -570,7 +571,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
}
|
||||
break;
|
||||
case ExoPlayerImplInternal.MSG_ERROR:
|
||||
playbackError = (ExoPlaybackException) msg.obj;
|
||||
ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
|
||||
this.playbackError = playbackError;
|
||||
for (Player.EventListener listener : listeners) {
|
||||
listener.onPlayerError(playbackError);
|
||||
}
|
||||
@ -652,7 +654,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
|
||||
boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
|
||||
boolean trackSelectorResultChanged =
|
||||
this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
|
||||
playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
|
||||
playbackInfo = newPlaybackInfo;
|
||||
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
|
||||
for (Player.EventListener listener : listeners) {
|
||||
|
@ -854,6 +854,9 @@ import java.util.Collections;
|
||||
}
|
||||
|
||||
private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
|
||||
if (message.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
message.getTarget().handleMessage(message.getType(), message.getPayload());
|
||||
} finally {
|
||||
@ -945,7 +948,7 @@ import java.util.Collections;
|
||||
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
|
||||
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
|
||||
sendMessageToTarget(nextInfo.message);
|
||||
if (nextInfo.message.getDeleteAfterDelivery()) {
|
||||
if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
|
||||
pendingMessages.remove(nextPendingMessageIndex);
|
||||
} else {
|
||||
nextPendingMessageIndex++;
|
||||
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.8.0";
|
||||
public static final String VERSION = "2.8.1";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.0";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2008000;
|
||||
public static final int VERSION_INT = 2008001;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
@ -87,7 +88,7 @@ public final class PlaybackParameters {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ public final class PlayerMessage {
|
||||
private boolean isSent;
|
||||
private boolean isDelivered;
|
||||
private boolean isProcessed;
|
||||
private boolean isCanceled;
|
||||
|
||||
/**
|
||||
* Creates a new message.
|
||||
@ -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.
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The configuration of a {@link Renderer}.
|
||||
*/
|
||||
@ -41,7 +43,7 @@ public final class RendererConfiguration {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
@ -71,7 +72,7 @@ public final class SeekParameters {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -92,6 +92,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
|
||||
private AudioAttributes audioAttributes;
|
||||
private float audioVolume;
|
||||
private MediaSource mediaSource;
|
||||
private List<Cue> currentCues;
|
||||
|
||||
/**
|
||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
||||
@ -177,6 +178,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
audioAttributes = AudioAttributes.DEFAULT;
|
||||
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
|
||||
currentCues = Collections.emptyList();
|
||||
|
||||
// Build the player and associated objects.
|
||||
player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
|
||||
@ -502,6 +504,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
|
||||
|
||||
@Override
|
||||
public void addTextOutput(TextOutput listener) {
|
||||
if (!currentCues.isEmpty()) {
|
||||
listener.onCues(currentCues);
|
||||
}
|
||||
textOutputs.add(listener);
|
||||
}
|
||||
|
||||
@ -775,6 +780,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
|
||||
mediaSource = null;
|
||||
analyticsCollector.resetForNewMediaSource();
|
||||
}
|
||||
currentCues = Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -790,6 +796,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
|
||||
if (mediaSource != null) {
|
||||
mediaSource.removeEventListener(analyticsCollector);
|
||||
}
|
||||
currentCues = Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1095,6 +1102,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
currentCues = cues;
|
||||
for (TextOutput textOutput : textOutputs) {
|
||||
textOutput.onCues(cues);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
||||
/**
|
||||
@ -119,7 +120,7 @@ public final class AudioAttributes {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@ -96,7 +97,7 @@ public final class AudioCapabilities {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
@ -338,7 +338,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof SchemeData)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
@ -92,7 +93,7 @@ public interface SeekMap {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/** Defines a seek point in a media stream. */
|
||||
public final class SeekPoint {
|
||||
|
||||
@ -42,7 +44,7 @@ public final class SeekPoint {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
@ -69,7 +70,7 @@ public interface TrackOutput {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -189,11 +189,13 @@ import java.util.List;
|
||||
}
|
||||
}
|
||||
|
||||
// True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio.
|
||||
boolean isRechunkable = sampleSizeBox.isFixedSampleSize()
|
||||
&& MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
|
||||
&& remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0
|
||||
&& remainingSynchronizationSamples == 0;
|
||||
// Fixed sample size raw audio may need to be rechunked.
|
||||
boolean isFixedSampleSizeRawAudio =
|
||||
sampleSizeBox.isFixedSampleSize()
|
||||
&& MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
|
||||
&& remainingTimestampDeltaChanges == 0
|
||||
&& remainingTimestampOffsetChanges == 0
|
||||
&& remainingSynchronizationSamples == 0;
|
||||
|
||||
long[] offsets;
|
||||
int[] sizes;
|
||||
@ -203,7 +205,7 @@ import java.util.List;
|
||||
long timestampTimeUnits = 0;
|
||||
long duration;
|
||||
|
||||
if (!isRechunkable) {
|
||||
if (!isFixedSampleSizeRawAudio) {
|
||||
offsets = new long[sampleCount];
|
||||
sizes = new int[sampleCount];
|
||||
timestamps = new long[sampleCount];
|
||||
@ -296,7 +298,8 @@ import java.util.List;
|
||||
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
|
||||
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
|
||||
}
|
||||
int fixedSampleSize = sampleSizeBox.readNextSampleSize();
|
||||
int fixedSampleSize =
|
||||
Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
|
||||
FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
|
||||
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
|
||||
offsets = rechunkedResults.offsets;
|
||||
@ -1224,7 +1227,7 @@ import java.util.List;
|
||||
stsc.setPosition(Atom.FULL_HEADER_SIZE);
|
||||
remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt();
|
||||
Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1");
|
||||
index = C.INDEX_UNSET;
|
||||
index = -1;
|
||||
}
|
||||
|
||||
public boolean moveNext() {
|
||||
|
@ -482,13 +482,13 @@ public final class MediaCodecUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger);
|
||||
if (profile == null) {
|
||||
int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1);
|
||||
if (profile == -1) {
|
||||
Log.w(TAG, "Unknown AVC profile: " + profileInteger);
|
||||
return null;
|
||||
}
|
||||
Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger);
|
||||
if (level == null) {
|
||||
int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1);
|
||||
if (level == -1) {
|
||||
Log.w(TAG, "Unknown AVC level: " + levelInteger);
|
||||
return null;
|
||||
}
|
||||
@ -639,7 +639,7 @@ public final class MediaCodecUtil {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -76,7 +77,7 @@ public final class Metadata implements Parcelable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.emsg;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
@ -104,7 +105,7 @@ public final class EventMessage implements Metadata.Entry {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -49,7 +50,7 @@ public final class ApicFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@ -37,7 +38,7 @@ public final class BinaryFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
@ -80,7 +81,7 @@ public final class ChapterFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -70,7 +71,7 @@ public final class ChapterTocFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
@ -45,7 +46,7 @@ public final class CommentFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -49,7 +50,7 @@ public final class GeobFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -43,7 +44,7 @@ public final class PrivFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
@ -40,7 +41,7 @@ public final class TextInformationFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
@ -40,7 +41,7 @@ public final class UrlLinkFrame extends Id3Frame {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ public abstract class DownloadAction {
|
||||
DownloaderConstructorHelper downloaderConstructorHelper);
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -250,7 +251,6 @@ public final class DownloadManager {
|
||||
Assertions.checkState(!released);
|
||||
Task task = addTaskForAction(action);
|
||||
if (initialized) {
|
||||
notifyListenersTaskStateChange(task);
|
||||
saveActions();
|
||||
maybeStartTasks();
|
||||
if (task.currentState == STATE_QUEUED) {
|
||||
@ -413,7 +413,6 @@ public final class DownloadManager {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
logd("Task state is changed", task);
|
||||
boolean stopped = !task.isActive();
|
||||
if (stopped) {
|
||||
activeDownloadTasks.remove(task);
|
||||
@ -430,6 +429,7 @@ public final class DownloadManager {
|
||||
}
|
||||
|
||||
private void notifyListenersTaskStateChange(Task task) {
|
||||
logd("Task state is changed", task);
|
||||
TaskState taskState = task.getDownloadState();
|
||||
for (Listener listener : listeners) {
|
||||
listener.onTaskStateChanged(this, taskState);
|
||||
@ -468,18 +468,16 @@ public final class DownloadManager {
|
||||
listener.onInitialized(DownloadManager.this);
|
||||
}
|
||||
if (!pendingTasks.isEmpty()) {
|
||||
for (int i = 0; i < pendingTasks.size(); i++) {
|
||||
tasks.add(pendingTasks.get(i));
|
||||
}
|
||||
tasks.addAll(pendingTasks);
|
||||
saveActions();
|
||||
}
|
||||
maybeStartTasks();
|
||||
for (int i = 0; i < pendingTasks.size(); i++) {
|
||||
Task pendingTask = pendingTasks.get(i);
|
||||
if (pendingTask.currentState == STATE_QUEUED) {
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
Task task = tasks.get(i);
|
||||
if (task.currentState == STATE_QUEUED) {
|
||||
// Task did not change out of its initial state, and so its initial state
|
||||
// won't have been reported to listeners. Do so now.
|
||||
notifyListenersTaskStateChange(pendingTask);
|
||||
notifyListenersTaskStateChange(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -699,9 +697,19 @@ public final class DownloadManager {
|
||||
+ ' '
|
||||
+ (action.isRemoveAction ? "remove" : "download")
|
||||
+ ' '
|
||||
+ toString(action.data)
|
||||
+ ' '
|
||||
+ getStateString();
|
||||
}
|
||||
|
||||
private static String toString(byte[] data) {
|
||||
if (data.length > 100) {
|
||||
return "<data is too long>";
|
||||
} else {
|
||||
return '\'' + Util.fromUtf8Bytes(data) + '\'';
|
||||
}
|
||||
}
|
||||
|
||||
private String getStateString() {
|
||||
switch (currentState) {
|
||||
case STATE_QUEUED_CANCELING:
|
||||
|
@ -84,7 +84,7 @@ public final class ProgressiveDownloadAction extends DownloadAction {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ public abstract class SegmentDownloadAction<K extends Comparable<K>> extends Dow
|
||||
protected abstract void writeKey(DataOutputStream output, K key) throws IOException;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ public interface MediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -96,7 +97,7 @@ public final class TrackGroup implements Parcelable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -98,7 +99,7 @@ public final class TrackGroupArray implements Parcelable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -78,6 +78,25 @@ public class Cue {
|
||||
*/
|
||||
public static final int LINE_TYPE_NUMBER = 1;
|
||||
|
||||
/** The type of default text size for this cue, which may be unset. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
TYPE_UNSET,
|
||||
TEXT_SIZE_TYPE_FRACTIONAL,
|
||||
TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
|
||||
TEXT_SIZE_TYPE_ABSOLUTE
|
||||
})
|
||||
public @interface TextSizeType {}
|
||||
|
||||
/** Text size is measured as a fraction of the viewport size minus the view padding. */
|
||||
public static final int TEXT_SIZE_TYPE_FRACTIONAL = 0;
|
||||
|
||||
/** Text size is measured as a fraction of the viewport size, ignoring the view padding */
|
||||
public static final int TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING = 1;
|
||||
|
||||
/** Text size is measured in number of pixels. */
|
||||
public static final int TEXT_SIZE_TYPE_ABSOLUTE = 2;
|
||||
|
||||
/**
|
||||
* The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated
|
||||
* with styling spans.
|
||||
@ -106,40 +125,39 @@ public class Cue {
|
||||
|
||||
/**
|
||||
* The type of the {@link #line} value.
|
||||
* <p>
|
||||
* {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
|
||||
*
|
||||
* <p>{@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the
|
||||
* viewport.
|
||||
* <p>
|
||||
* {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each
|
||||
* line is taken to be the size of the first line of the cue. When {@link #line} is greater than
|
||||
* or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset from
|
||||
* the start edge. When {@link #line} is negative lines count from the end of the viewport, with
|
||||
* -1 indicating zero offset from the end edge. For horizontal text the line spacing is the height
|
||||
* of the first line of the cue, and the start and end of the viewport are the top and bottom
|
||||
* respectively.
|
||||
* <p>
|
||||
* Note that it's particularly important to consider the effect of {@link #lineAnchor} when using
|
||||
* {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)} positions a
|
||||
* (potentially multi-line) cue at the very top of the viewport.
|
||||
* {@code (line == -1 && lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue
|
||||
* at the very bottom of the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)}
|
||||
* and {@code (line == -1 && lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of
|
||||
* the viewport. {@code (line == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only
|
||||
* the last line is visible at the top of the viewport.
|
||||
* {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first
|
||||
* line is visible at the bottom of the viewport.
|
||||
*
|
||||
* <p>{@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of
|
||||
* each line is taken to be the size of the first line of the cue. When {@link #line} is greater
|
||||
* than or equal to 0 lines count from the start of the viewport, with 0 indicating zero offset
|
||||
* from the start edge. When {@link #line} is negative lines count from the end of the viewport,
|
||||
* with -1 indicating zero offset from the end edge. For horizontal text the line spacing is the
|
||||
* height of the first line of the cue, and the start and end of the viewport are the top and
|
||||
* bottom respectively.
|
||||
*
|
||||
* <p>Note that it's particularly important to consider the effect of {@link #lineAnchor} when
|
||||
* using {@link #LINE_TYPE_NUMBER}. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_START)}
|
||||
* positions a (potentially multi-line) cue at the very top of the viewport. {@code (line == -1 &&
|
||||
* lineAnchor == ANCHOR_TYPE_END)} positions a (potentially multi-line) cue at the very bottom of
|
||||
* the viewport. {@code (line == 0 && lineAnchor == ANCHOR_TYPE_END)} and {@code (line == -1 &&
|
||||
* lineAnchor == ANCHOR_TYPE_START)} position cues entirely outside of the viewport. {@code (line
|
||||
* == 1 && lineAnchor == ANCHOR_TYPE_END)} positions a cue so that only the last line is visible
|
||||
* at the top of the viewport. {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a
|
||||
* cue so that only its first line is visible at the bottom of the viewport.
|
||||
*/
|
||||
@LineType public final int lineType;
|
||||
public final @LineType int lineType;
|
||||
|
||||
/**
|
||||
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
|
||||
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
* <p>
|
||||
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
|
||||
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
|
||||
* respectively.
|
||||
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, {@link
|
||||
* #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
*
|
||||
* <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
|
||||
* #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of
|
||||
* the cue box respectively.
|
||||
*/
|
||||
@AnchorType public final int lineAnchor;
|
||||
public final @AnchorType int lineAnchor;
|
||||
|
||||
/**
|
||||
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
|
||||
@ -152,14 +170,14 @@ public class Cue {
|
||||
public final float position;
|
||||
|
||||
/**
|
||||
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START},
|
||||
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
* <p>
|
||||
* For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE}
|
||||
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
|
||||
* respectively.
|
||||
* The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, {@link
|
||||
* #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
*
|
||||
* <p>For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link
|
||||
* #ANCHOR_TYPE_MIDDLE} and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of
|
||||
* the cue box respectively.
|
||||
*/
|
||||
@AnchorType public final int positionAnchor;
|
||||
public final @AnchorType int positionAnchor;
|
||||
|
||||
/**
|
||||
* The size of the cue box in the writing direction specified as a fraction of the viewport size
|
||||
@ -184,6 +202,18 @@ public class Cue {
|
||||
*/
|
||||
public final int windowColor;
|
||||
|
||||
/**
|
||||
* The default text size type for this cue's text, or {@link #TYPE_UNSET} if this cue has no
|
||||
* default text size.
|
||||
*/
|
||||
public final @TextSizeType int textSizeType;
|
||||
|
||||
/**
|
||||
* The default text size for this cue's text, or {@link #DIMEN_UNSET} if this cue has no default
|
||||
* text size.
|
||||
*/
|
||||
public final float textSize;
|
||||
|
||||
/**
|
||||
* Creates an image cue.
|
||||
*
|
||||
@ -194,17 +224,36 @@ public class Cue {
|
||||
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
* @param verticalPosition The position of the vertical anchor within the viewport, expressed as a
|
||||
* fraction of the viewport height.
|
||||
* @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START},
|
||||
* {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
* @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, {@link
|
||||
* #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}.
|
||||
* @param width The width of the cue as a fraction of the viewport width.
|
||||
* @param height The height of the cue as a fraction of the viewport height, or
|
||||
* {@link #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the
|
||||
* specified {@code width}.
|
||||
* @param height The height of the cue as a fraction of the viewport height, or {@link
|
||||
* #DIMEN_UNSET} if the bitmap should be displayed at its natural height for the specified
|
||||
* {@code width}.
|
||||
*/
|
||||
public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor,
|
||||
float verticalPosition, @AnchorType int verticalPositionAnchor, float width, float height) {
|
||||
this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor,
|
||||
horizontalPosition, horizontalPositionAnchor, width, height, false, Color.BLACK);
|
||||
public Cue(
|
||||
Bitmap bitmap,
|
||||
float horizontalPosition,
|
||||
@AnchorType int horizontalPositionAnchor,
|
||||
float verticalPosition,
|
||||
@AnchorType int verticalPositionAnchor,
|
||||
float width,
|
||||
float height) {
|
||||
this(
|
||||
/* text= */ null,
|
||||
/* textAlignment= */ null,
|
||||
bitmap,
|
||||
verticalPosition,
|
||||
/* lineType= */ LINE_TYPE_FRACTION,
|
||||
verticalPositionAnchor,
|
||||
horizontalPosition,
|
||||
horizontalPositionAnchor,
|
||||
/* textSizeType= */ TYPE_UNSET,
|
||||
/* textSize= */ DIMEN_UNSET,
|
||||
width,
|
||||
height,
|
||||
/* windowColorSet= */ false,
|
||||
/* windowColor= */ Color.BLACK);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,7 +263,15 @@ public class Cue {
|
||||
* @param text See {@link #text}.
|
||||
*/
|
||||
public Cue(CharSequence text) {
|
||||
this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
|
||||
this(
|
||||
text,
|
||||
/* textAlignment= */ null,
|
||||
/* line= */ DIMEN_UNSET,
|
||||
/* lineType= */ TYPE_UNSET,
|
||||
/* lineAnchor= */ TYPE_UNSET,
|
||||
/* position= */ DIMEN_UNSET,
|
||||
/* positionAnchor= */ TYPE_UNSET,
|
||||
/* size= */ DIMEN_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,10 +286,68 @@ public class Cue {
|
||||
* @param positionAnchor See {@link #positionAnchor}.
|
||||
* @param size See {@link #size}.
|
||||
*/
|
||||
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
|
||||
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) {
|
||||
this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false,
|
||||
Color.BLACK);
|
||||
public Cue(
|
||||
CharSequence text,
|
||||
Alignment textAlignment,
|
||||
float line,
|
||||
@LineType int lineType,
|
||||
@AnchorType int lineAnchor,
|
||||
float position,
|
||||
@AnchorType int positionAnchor,
|
||||
float size) {
|
||||
this(
|
||||
text,
|
||||
textAlignment,
|
||||
line,
|
||||
lineType,
|
||||
lineAnchor,
|
||||
position,
|
||||
positionAnchor,
|
||||
size,
|
||||
/* windowColorSet= */ false,
|
||||
/* windowColor= */ Color.BLACK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a text cue.
|
||||
*
|
||||
* @param text See {@link #text}.
|
||||
* @param textAlignment See {@link #textAlignment}.
|
||||
* @param line See {@link #line}.
|
||||
* @param lineType See {@link #lineType}.
|
||||
* @param lineAnchor See {@link #lineAnchor}.
|
||||
* @param position See {@link #position}.
|
||||
* @param positionAnchor See {@link #positionAnchor}.
|
||||
* @param size See {@link #size}.
|
||||
* @param textSizeType See {@link #textSizeType}.
|
||||
* @param textSize See {@link #textSize}.
|
||||
*/
|
||||
public Cue(
|
||||
CharSequence text,
|
||||
Alignment textAlignment,
|
||||
float line,
|
||||
@LineType int lineType,
|
||||
@AnchorType int lineAnchor,
|
||||
float position,
|
||||
@AnchorType int positionAnchor,
|
||||
float size,
|
||||
@TextSizeType int textSizeType,
|
||||
float textSize) {
|
||||
this(
|
||||
text,
|
||||
textAlignment,
|
||||
/* bitmap= */ null,
|
||||
line,
|
||||
lineType,
|
||||
lineAnchor,
|
||||
position,
|
||||
positionAnchor,
|
||||
textSizeType,
|
||||
textSize,
|
||||
size,
|
||||
/* bitmapHeight= */ DIMEN_UNSET,
|
||||
/* windowColorSet= */ false,
|
||||
/* windowColor= */ Color.BLACK);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -249,16 +364,48 @@ public class Cue {
|
||||
* @param windowColorSet See {@link #windowColorSet}.
|
||||
* @param windowColor See {@link #windowColor}.
|
||||
*/
|
||||
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
|
||||
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
|
||||
boolean windowColorSet, int windowColor) {
|
||||
this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size,
|
||||
DIMEN_UNSET, windowColorSet, windowColor);
|
||||
public Cue(
|
||||
CharSequence text,
|
||||
Alignment textAlignment,
|
||||
float line,
|
||||
@LineType int lineType,
|
||||
@AnchorType int lineAnchor,
|
||||
float position,
|
||||
@AnchorType int positionAnchor,
|
||||
float size,
|
||||
boolean windowColorSet,
|
||||
int windowColor) {
|
||||
this(
|
||||
text,
|
||||
textAlignment,
|
||||
/* bitmap= */ null,
|
||||
line,
|
||||
lineType,
|
||||
lineAnchor,
|
||||
position,
|
||||
positionAnchor,
|
||||
/* textSizeType= */ TYPE_UNSET,
|
||||
/* textSize= */ DIMEN_UNSET,
|
||||
size,
|
||||
/* bitmapHeight= */ DIMEN_UNSET,
|
||||
windowColorSet,
|
||||
windowColor);
|
||||
}
|
||||
|
||||
private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line,
|
||||
@LineType int lineType, @AnchorType int lineAnchor, float position,
|
||||
@AnchorType int positionAnchor, float size, float bitmapHeight, boolean windowColorSet,
|
||||
private Cue(
|
||||
CharSequence text,
|
||||
Alignment textAlignment,
|
||||
Bitmap bitmap,
|
||||
float line,
|
||||
@LineType int lineType,
|
||||
@AnchorType int lineAnchor,
|
||||
float position,
|
||||
@AnchorType int positionAnchor,
|
||||
@TextSizeType int textSizeType,
|
||||
float textSize,
|
||||
float size,
|
||||
float bitmapHeight,
|
||||
boolean windowColorSet,
|
||||
int windowColor) {
|
||||
this.text = text;
|
||||
this.textAlignment = textAlignment;
|
||||
@ -272,6 +419,8 @@ public class Cue {
|
||||
this.bitmapHeight = bitmapHeight;
|
||||
this.windowColorSet = windowColorSet;
|
||||
this.windowColor = windowColor;
|
||||
this.textSizeType = textSizeType;
|
||||
this.textSize = textSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
||||
/**
|
||||
* A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
|
||||
* supported by this decoder are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>content
|
||||
* <li>core
|
||||
@ -51,7 +52,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
||||
* <li>time-clock
|
||||
* <li>time-offset-with-frames
|
||||
* <li>time-offset-with-ticks
|
||||
* <li>cell-resolution
|
||||
* </ul>
|
||||
*
|
||||
* @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
|
||||
*/
|
||||
public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
@ -74,11 +77,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
|
||||
private static final Pattern PERCENTAGE_COORDINATES =
|
||||
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
|
||||
private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
|
||||
|
||||
private static final int DEFAULT_FRAME_RATE = 30;
|
||||
|
||||
private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
|
||||
new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
|
||||
private static final CellResolution DEFAULT_CELL_RESOLUTION =
|
||||
new CellResolution(/* columns= */ 32, /* rows= */ 15);
|
||||
|
||||
private final XmlPullParserFactory xmlParserFactory;
|
||||
|
||||
@ -107,6 +113,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
int unsupportedNodeDepth = 0;
|
||||
int eventType = xmlParser.getEventType();
|
||||
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
|
||||
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
TtmlNode parent = nodeStack.peekLast();
|
||||
if (unsupportedNodeDepth == 0) {
|
||||
@ -114,12 +121,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
if (TtmlNode.TAG_TT.equals(name)) {
|
||||
frameAndTickRate = parseFrameAndTickRates(xmlParser);
|
||||
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
|
||||
}
|
||||
if (!isSupportedTag(name)) {
|
||||
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
|
||||
unsupportedNodeDepth++;
|
||||
} else if (TtmlNode.TAG_HEAD.equals(name)) {
|
||||
parseHeader(xmlParser, globalStyles, regionMap);
|
||||
parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
|
||||
} else {
|
||||
try {
|
||||
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
|
||||
@ -193,8 +201,36 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
|
||||
}
|
||||
|
||||
private Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser,
|
||||
Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions)
|
||||
private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
|
||||
throws SubtitleDecoderException {
|
||||
String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
|
||||
if (cellResolution == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution);
|
||||
if (!cellResolutionMatcher.matches()) {
|
||||
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
int columns = Integer.parseInt(cellResolutionMatcher.group(1));
|
||||
int rows = Integer.parseInt(cellResolutionMatcher.group(2));
|
||||
if (columns == 0 || rows == 0) {
|
||||
throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows);
|
||||
}
|
||||
return new CellResolution(columns, rows);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, TtmlStyle> parseHeader(
|
||||
XmlPullParser xmlParser,
|
||||
Map<String, TtmlStyle> globalStyles,
|
||||
Map<String, TtmlRegion> globalRegions,
|
||||
CellResolution cellResolution)
|
||||
throws IOException, XmlPullParserException {
|
||||
do {
|
||||
xmlParser.next();
|
||||
@ -210,7 +246,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
globalStyles.put(style.getId(), style);
|
||||
}
|
||||
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
|
||||
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser);
|
||||
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
|
||||
if (ttmlRegion != null) {
|
||||
globalRegions.put(ttmlRegion.id, ttmlRegion);
|
||||
}
|
||||
@ -221,12 +257,12 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
|
||||
/**
|
||||
* Parses a region declaration.
|
||||
* <p>
|
||||
* If the region defines an origin and extent, it is required that they're defined as percentages
|
||||
* of the viewport. Region declarations that define origin and extent in other formats are
|
||||
* unsupported, and null is returned.
|
||||
*
|
||||
* <p>If the region defines an origin and extent, it is required that they're defined as
|
||||
* percentages of the viewport. Region declarations that define origin and extent in other formats
|
||||
* are unsupported, and null is returned.
|
||||
*/
|
||||
private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) {
|
||||
private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) {
|
||||
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
|
||||
if (regionId == null) {
|
||||
return null;
|
||||
@ -305,7 +341,16 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width);
|
||||
float regionTextHeight = 1.0f / cellResolution.rows;
|
||||
return new TtmlRegion(
|
||||
regionId,
|
||||
position,
|
||||
line,
|
||||
/* lineType= */ Cue.LINE_TYPE_FRACTION,
|
||||
lineAnchor,
|
||||
width,
|
||||
/* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
|
||||
/* textSize= */ regionTextHeight);
|
||||
}
|
||||
|
||||
private String[] parseStyleIds(String parentStyleIds) {
|
||||
@ -594,4 +639,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||
this.tickRate = tickRate;
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents the cell resolution for a TTML file. */
|
||||
private static final class CellResolution {
|
||||
final int columns;
|
||||
final int rows;
|
||||
|
||||
CellResolution(int columns, int rows) {
|
||||
this.columns = columns;
|
||||
this.rows = rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,35 +175,51 @@ import java.util.TreeSet;
|
||||
Map<String, TtmlRegion> regionMap) {
|
||||
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
|
||||
traverseForText(timeUs, false, regionId, regionOutputs);
|
||||
traverseForStyle(globalStyles, regionOutputs);
|
||||
traverseForStyle(timeUs, globalStyles, regionOutputs);
|
||||
List<Cue> cues = new ArrayList<>();
|
||||
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
|
||||
TtmlRegion region = regionMap.get(entry.getKey());
|
||||
cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
|
||||
region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width));
|
||||
cues.add(
|
||||
new Cue(
|
||||
cleanUpText(entry.getValue()),
|
||||
/* textAlignment= */ null,
|
||||
region.line,
|
||||
region.lineType,
|
||||
region.lineAnchor,
|
||||
region.position,
|
||||
/* positionAnchor= */ Cue.TYPE_UNSET,
|
||||
region.width,
|
||||
region.textSizeType,
|
||||
region.textSize));
|
||||
}
|
||||
return cues;
|
||||
}
|
||||
|
||||
private void traverseForText(long timeUs, boolean descendsPNode,
|
||||
String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
private void traverseForText(
|
||||
long timeUs,
|
||||
boolean descendsPNode,
|
||||
String inheritedRegion,
|
||||
Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
nodeStartsByRegion.clear();
|
||||
nodeEndsByRegion.clear();
|
||||
String resolvedRegionId = regionId;
|
||||
if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) {
|
||||
resolvedRegionId = inheritedRegion;
|
||||
if (TAG_METADATA.equals(tag)) {
|
||||
// Ignore metadata tag.
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
|
||||
|
||||
if (isTextNode && descendsPNode) {
|
||||
getRegionOutput(resolvedRegionId, regionOutputs).append(text);
|
||||
} else if (TAG_BR.equals(tag) && descendsPNode) {
|
||||
getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
|
||||
} else if (TAG_METADATA.equals(tag)) {
|
||||
// Do nothing.
|
||||
} else if (isActive(timeUs)) {
|
||||
boolean isPNode = TAG_P.equals(tag);
|
||||
// This is a container node, which can contain zero or more children.
|
||||
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
|
||||
nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
|
||||
}
|
||||
|
||||
boolean isPNode = TAG_P.equals(tag);
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
|
||||
regionOutputs);
|
||||
@ -211,39 +227,50 @@ import java.util.TreeSet;
|
||||
if (isPNode) {
|
||||
TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
|
||||
}
|
||||
|
||||
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
|
||||
nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SpannableStringBuilder getRegionOutput(String resolvedRegionId,
|
||||
Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
private static SpannableStringBuilder getRegionOutput(
|
||||
String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
if (!regionOutputs.containsKey(resolvedRegionId)) {
|
||||
regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
|
||||
}
|
||||
return regionOutputs.get(resolvedRegionId);
|
||||
}
|
||||
|
||||
private void traverseForStyle(Map<String, TtmlStyle> globalStyles,
|
||||
private void traverseForStyle(
|
||||
long timeUs,
|
||||
Map<String, TtmlStyle> globalStyles,
|
||||
Map<String, SpannableStringBuilder> regionOutputs) {
|
||||
if (!isActive(timeUs)) {
|
||||
return;
|
||||
}
|
||||
for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
|
||||
String regionId = entry.getKey();
|
||||
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
|
||||
applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue());
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
getChild(i).traverseForStyle(globalStyles, regionOutputs);
|
||||
int end = entry.getValue();
|
||||
if (start != end) {
|
||||
SpannableStringBuilder regionOutput = regionOutputs.get(regionId);
|
||||
applyStyleToOutput(globalStyles, regionOutput, start, end);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyStyleToOutput(Map<String, TtmlStyle> globalStyles,
|
||||
SpannableStringBuilder regionOutput, int start, int end) {
|
||||
if (start != end) {
|
||||
TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
|
||||
if (resolvedStyle != null) {
|
||||
TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
|
||||
}
|
||||
private void applyStyleToOutput(
|
||||
Map<String, TtmlStyle> globalStyles,
|
||||
SpannableStringBuilder regionOutput,
|
||||
int start,
|
||||
int end) {
|
||||
TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
|
||||
if (resolvedStyle != null) {
|
||||
TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,22 +25,41 @@ import com.google.android.exoplayer2.text.Cue;
|
||||
public final String id;
|
||||
public final float position;
|
||||
public final float line;
|
||||
@Cue.LineType public final int lineType;
|
||||
@Cue.AnchorType public final int lineAnchor;
|
||||
public final @Cue.LineType int lineType;
|
||||
public final @Cue.AnchorType int lineAnchor;
|
||||
public final float width;
|
||||
public final @Cue.TextSizeType int textSizeType;
|
||||
public final float textSize;
|
||||
|
||||
public TtmlRegion(String id) {
|
||||
this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
|
||||
this(
|
||||
id,
|
||||
/* position= */ Cue.DIMEN_UNSET,
|
||||
/* line= */ Cue.DIMEN_UNSET,
|
||||
/* lineType= */ Cue.TYPE_UNSET,
|
||||
/* lineAnchor= */ Cue.TYPE_UNSET,
|
||||
/* width= */ Cue.DIMEN_UNSET,
|
||||
/* textSizeType= */ Cue.TYPE_UNSET,
|
||||
/* textSize= */ Cue.DIMEN_UNSET);
|
||||
}
|
||||
|
||||
public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType,
|
||||
@Cue.AnchorType int lineAnchor, float width) {
|
||||
public TtmlRegion(
|
||||
String id,
|
||||
float position,
|
||||
float line,
|
||||
@Cue.LineType int lineType,
|
||||
@Cue.AnchorType int lineAnchor,
|
||||
float width,
|
||||
int textSizeType,
|
||||
float textSize) {
|
||||
this.id = id;
|
||||
this.position = position;
|
||||
this.line = line;
|
||||
this.lineType = lineType;
|
||||
this.lineAnchor = lineAnchor;
|
||||
this.width = width;
|
||||
this.textSizeType = textSizeType;
|
||||
this.textSize = textSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
@ -183,7 +184,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import android.graphics.Point;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
@ -771,7 +772,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
@ -992,7 +993,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
@ -2020,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
@ -2074,7 +2075,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.trackselection;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** An array of {@link TrackSelection}s. */
|
||||
@ -64,7 +65,7 @@ public final class TrackSelectionArray {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.upstream;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -79,7 +80,7 @@ public interface DataSource {
|
||||
*
|
||||
* @return The {@link Uri} from which data is being read, or null if the source is not open.
|
||||
*/
|
||||
Uri getUri();
|
||||
@Nullable Uri getUri();
|
||||
|
||||
/**
|
||||
* Closes the source.
|
||||
|
@ -61,7 +61,7 @@ public final class DataSpec {
|
||||
/**
|
||||
* Body for a POST request, null otherwise.
|
||||
*/
|
||||
public final byte[] postBody;
|
||||
public final @Nullable byte[] postBody;
|
||||
/**
|
||||
* The absolute position of the data in the full stream.
|
||||
*/
|
||||
@ -81,12 +81,12 @@ public final class DataSpec {
|
||||
* A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
|
||||
* {@link DataSpec} is not intended to be used in conjunction with a cache.
|
||||
*/
|
||||
@Nullable public final String key;
|
||||
public final @Nullable String key;
|
||||
/**
|
||||
* Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
|
||||
* {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
|
||||
*/
|
||||
@Flags public final int flags;
|
||||
public final @Flags int flags;
|
||||
|
||||
/**
|
||||
* Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
|
||||
@ -128,7 +128,8 @@ public final class DataSpec {
|
||||
* @param key {@link #key}.
|
||||
* @param flags {@link #flags}.
|
||||
*/
|
||||
public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, @Flags int flags) {
|
||||
public DataSpec(
|
||||
Uri uri, long absoluteStreamPosition, long length, @Nullable String key, @Flags int flags) {
|
||||
this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
|
||||
}
|
||||
|
||||
@ -143,7 +144,12 @@ public final class DataSpec {
|
||||
* @param key {@link #key}.
|
||||
* @param flags {@link #flags}.
|
||||
*/
|
||||
public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key,
|
||||
public DataSpec(
|
||||
Uri uri,
|
||||
long absoluteStreamPosition,
|
||||
long position,
|
||||
long length,
|
||||
@Nullable String key,
|
||||
@Flags int flags) {
|
||||
this(uri, null, absoluteStreamPosition, position, length, key, flags);
|
||||
}
|
||||
@ -162,7 +168,7 @@ public final class DataSpec {
|
||||
*/
|
||||
public DataSpec(
|
||||
Uri uri,
|
||||
byte[] postBody,
|
||||
@Nullable byte[] postBody,
|
||||
long absoluteStreamPosition,
|
||||
long position,
|
||||
long length,
|
||||
@ -222,4 +228,13 @@ public final class DataSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this {@link DataSpec} with the specified Uri.
|
||||
*
|
||||
* @param uri The new source {@link Uri}.
|
||||
* @return The copied {@link DataSpec} with the specified Uri.
|
||||
*/
|
||||
public DataSpec withUri(Uri uri) {
|
||||
return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.upstream;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
||||
import java.io.IOException;
|
||||
@ -63,7 +64,7 @@ public final class PriorityDataSource implements DataSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
public @Nullable Uri getUri() {
|
||||
return upstream.getUri();
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.DataSink;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
@ -51,6 +52,8 @@ public final class CacheDataSource implements DataSource {
|
||||
*/
|
||||
public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
private static final String TAG = "CacheDataSource";
|
||||
|
||||
/**
|
||||
* Flags controlling the cache's behavior.
|
||||
*/
|
||||
@ -218,7 +221,7 @@ public final class CacheDataSource implements DataSource {
|
||||
try {
|
||||
key = CacheUtil.getKey(dataSpec);
|
||||
uri = dataSpec.uri;
|
||||
actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
|
||||
actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri);
|
||||
flags = dataSpec.flags;
|
||||
readPosition = dataSpec.position;
|
||||
|
||||
@ -269,7 +272,7 @@ public final class CacheDataSource implements DataSource {
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
} else if (currentDataSpecLengthUnset) {
|
||||
setNoBytesRemainingAndMaybeStoreLength();
|
||||
setBytesRemainingAndMaybeStoreLength(0);
|
||||
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
|
||||
closeCurrentSource();
|
||||
openNextSource(false);
|
||||
@ -278,7 +281,7 @@ public final class CacheDataSource implements DataSource {
|
||||
return bytesRead;
|
||||
} catch (IOException e) {
|
||||
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
|
||||
setNoBytesRemainingAndMaybeStoreLength();
|
||||
setBytesRemainingAndMaybeStoreLength(0);
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
handleBeforeThrow(e);
|
||||
@ -399,38 +402,46 @@ public final class CacheDataSource implements DataSource {
|
||||
currentDataSource = nextDataSource;
|
||||
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
|
||||
long resolvedLength = nextDataSource.open(nextDataSpec);
|
||||
|
||||
// Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
|
||||
ContentMetadataMutations mutations = new ContentMetadataMutations();
|
||||
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
|
||||
bytesRemaining = resolvedLength;
|
||||
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
|
||||
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();
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.DataInputStream;
|
||||
@ -236,7 +237,7 @@ import java.util.TreeSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -26,7 +25,6 @@ import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@ -53,8 +51,6 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
private static final int FLAG_ENCRYPTED_INDEX = 1;
|
||||
|
||||
private static final String TAG = "CachedContentIndex";
|
||||
|
||||
private final HashMap<String, CachedContent> keyToContent;
|
||||
private final SparseArray<String> idToKey;
|
||||
private final AtomicFile atomicFile;
|
||||
@ -248,13 +244,12 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
add(cachedContent);
|
||||
hashCode += cachedContent.headerHashCode(version);
|
||||
}
|
||||
if (input.readInt() != hashCode) {
|
||||
int fileHashCode = input.readInt();
|
||||
boolean isEOF = input.read() == -1;
|
||||
if (fileHashCode != hashCode || !isEOF) {
|
||||
return false;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error reading cache content index file.", e);
|
||||
return false;
|
||||
} finally {
|
||||
if (input != null) {
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
@ -131,7 +132,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
||||
/**
|
||||
* Holder for FLAC stream info.
|
||||
*/
|
||||
@ -52,8 +54,29 @@ public final class FlacStreamInfo {
|
||||
// Remaining 16 bytes is md5 value
|
||||
}
|
||||
|
||||
public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize,
|
||||
int sampleRate, int channels, int bitsPerSample, long totalSamples) {
|
||||
/**
|
||||
* Constructs a FlacStreamInfo given the parameters.
|
||||
*
|
||||
* @param minBlockSize Minimum block size of the FLAC stream.
|
||||
* @param maxBlockSize Maximum block size of the FLAC stream.
|
||||
* @param minFrameSize Minimum frame size of the FLAC stream.
|
||||
* @param maxFrameSize Maximum frame size of the FLAC stream.
|
||||
* @param sampleRate Sample rate of the FLAC stream.
|
||||
* @param channels Number of channels of the FLAC stream.
|
||||
* @param bitsPerSample Number of bits per sample of the FLAC stream.
|
||||
* @param totalSamples Total samples of the FLAC stream.
|
||||
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
|
||||
* METADATA_BLOCK_STREAMINFO</a>
|
||||
*/
|
||||
public FlacStreamInfo(
|
||||
int minBlockSize,
|
||||
int maxBlockSize,
|
||||
int minFrameSize,
|
||||
int maxFrameSize,
|
||||
int sampleRate,
|
||||
int channels,
|
||||
int bitsPerSample,
|
||||
long totalSamples) {
|
||||
this.minBlockSize = minBlockSize;
|
||||
this.maxBlockSize = maxBlockSize;
|
||||
this.minFrameSize = minFrameSize;
|
||||
@ -64,16 +87,43 @@ public final class FlacStreamInfo {
|
||||
this.totalSamples = totalSamples;
|
||||
}
|
||||
|
||||
/** Returns the maximum size for a decoded frame from the FLAC stream. */
|
||||
public int maxDecodedFrameSize() {
|
||||
return maxBlockSize * channels * (bitsPerSample / 8);
|
||||
}
|
||||
|
||||
/** Returns the bit-rate of the FLAC stream. */
|
||||
public int bitRate() {
|
||||
return bitsPerSample * sampleRate;
|
||||
}
|
||||
|
||||
/** Returns the duration of the FLAC stream in microseconds. */
|
||||
public long durationUs() {
|
||||
return (totalSamples * 1000000L) / sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample index for the sample at given position.
|
||||
*
|
||||
* @param timeUs Time position in microseconds in the FLAC stream.
|
||||
* @return The sample index for the sample at given position.
|
||||
*/
|
||||
public long getSampleIndex(long timeUs) {
|
||||
long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
|
||||
return Util.constrainValue(sampleIndex, 0, totalSamples - 1);
|
||||
}
|
||||
|
||||
/** Returns the approximate number of bytes per frame for the current FLAC stream. */
|
||||
public long getApproxBytesPerFrame() {
|
||||
long approxBytesPerFrame;
|
||||
if (maxFrameSize > 0) {
|
||||
approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;
|
||||
} else {
|
||||
// Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the
|
||||
// default value for FLAC block-size, which is 4096.
|
||||
long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096;
|
||||
approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64;
|
||||
}
|
||||
return approxBytesPerFrame;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -17,10 +17,10 @@ package com.google.android.exoplayer2.video;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@ -85,7 +85,7 @@ public final class ColorInfo implements Parcelable {
|
||||
|
||||
// Parcelable implementation.
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
@ -15,29 +15,29 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.video;
|
||||
|
||||
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_NONE;
|
||||
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_PROTECTED_PBUFFER;
|
||||
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.graphics.SurfaceTexture.OnFrameAvailableListener;
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.EGLConfig;
|
||||
import android.opengl.EGLContext;
|
||||
import android.opengl.EGLDisplay;
|
||||
import android.opengl.EGLSurface;
|
||||
import android.opengl.GLES20;
|
||||
import android.os.Handler;
|
||||
import android.os.Handler.Callback;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.EGLSurfaceTexture;
|
||||
import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A dummy {@link Surface}.
|
||||
@ -50,16 +50,6 @@ public final class DummySurface extends Surface {
|
||||
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
|
||||
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
|
||||
private @interface SecureMode {}
|
||||
|
||||
private static final int SECURE_MODE_NONE = 0;
|
||||
private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;
|
||||
private static final int SECURE_MODE_PROTECTED_PBUFFER = 2;
|
||||
|
||||
/**
|
||||
* Whether the surface is secure.
|
||||
*/
|
||||
@ -161,32 +151,25 @@ public final class DummySurface extends Surface {
|
||||
: SECURE_MODE_PROTECTED_PBUFFER;
|
||||
}
|
||||
|
||||
private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener,
|
||||
Callback {
|
||||
private static class DummySurfaceThread extends HandlerThread implements Callback {
|
||||
|
||||
private static final int MSG_INIT = 1;
|
||||
private static final int MSG_UPDATE_TEXTURE = 2;
|
||||
private static final int MSG_RELEASE = 3;
|
||||
private 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1178,6 +1178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
// https://github.com/google/ExoPlayer/issues/4006,
|
||||
// https://github.com/google/ExoPlayer/issues/4084,
|
||||
// https://github.com/google/ExoPlayer/issues/4104.
|
||||
// https://github.com/google/ExoPlayer/issues/4134.
|
||||
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|
||||
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|
||||
|| "mido".equals(Util.DEVICE) // Redmi Note 4
|
||||
@ -1190,7 +1191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
|| "F3311".equals(Util.DEVICE) // Sony Xperia E5
|
||||
|| "M5c".equals(Util.DEVICE) // Meizu M5C
|
||||
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
|
||||
|| "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note
|
||||
|| "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note
|
||||
|| "woods_f".equals(Util.MODEL)) // Moto E (4)
|
||||
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|
||||
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|
||||
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II
|
||||
|
@ -51,6 +51,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
@ -1812,6 +1813,88 @@ public final class ExoPlayerTest {
|
||||
assertThat(target3.windowIndex).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelMessageBeforeDelivery() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
|
||||
final AtomicReference<PlayerMessage> message = new AtomicReference<>();
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder("testCancelMessage")
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_BUFFERING)
|
||||
.executeRunnable(
|
||||
new PlayerRunnable() {
|
||||
@Override
|
||||
public void run(SimpleExoPlayer player) {
|
||||
message.set(
|
||||
player.createMessage(target).setPosition(/* positionMs= */ 50).send());
|
||||
}
|
||||
})
|
||||
// Play a bit to ensure message arrived in internal player.
|
||||
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 30)
|
||||
.executeRunnable(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
message.get().cancel();
|
||||
}
|
||||
})
|
||||
.play()
|
||||
.build();
|
||||
new Builder()
|
||||
.setTimeline(timeline)
|
||||
.setActionSchedule(actionSchedule)
|
||||
.build()
|
||||
.start()
|
||||
.blockUntilEnded(TIMEOUT_MS);
|
||||
assertThat(message.get().isCanceled()).isTrue();
|
||||
assertThat(target.messageCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelRepeatedMessageAfterDelivery() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
|
||||
final AtomicReference<PlayerMessage> message = new AtomicReference<>();
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder("testCancelMessage")
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_BUFFERING)
|
||||
.executeRunnable(
|
||||
new PlayerRunnable() {
|
||||
@Override
|
||||
public void run(SimpleExoPlayer player) {
|
||||
message.set(
|
||||
player
|
||||
.createMessage(target)
|
||||
.setPosition(/* positionMs= */ 50)
|
||||
.setDeleteAfterDelivery(/* deleteAfterDelivery= */ false)
|
||||
.send());
|
||||
}
|
||||
})
|
||||
// Play until the message has been delivered.
|
||||
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 51)
|
||||
// Seek back, cancel the message, and play past the same position again.
|
||||
.seek(/* positionMs= */ 0)
|
||||
.executeRunnable(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
message.get().cancel();
|
||||
}
|
||||
})
|
||||
.play()
|
||||
.build();
|
||||
new Builder()
|
||||
.setTimeline(timeline)
|
||||
.setActionSchedule(actionSchedule)
|
||||
.build()
|
||||
.start()
|
||||
.blockUntilEnded(TIMEOUT_MS);
|
||||
assertThat(message.get().isCanceled()).isTrue();
|
||||
assertThat(target.messageCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAndSwitchSurface() throws Exception {
|
||||
final List<Integer> rendererMessages = new ArrayList<>();
|
||||
@ -1934,8 +2017,10 @@ public final class ExoPlayerTest {
|
||||
|
||||
@Override
|
||||
public void handleMessage(SimpleExoPlayer player, int messageType, Object message) {
|
||||
windowIndex = player.getCurrentWindowIndex();
|
||||
positionMs = player.getCurrentPosition();
|
||||
if (player != null) {
|
||||
windowIndex = player.getCurrentWindowIndex();
|
||||
positionMs = player.getCurrentPosition();
|
||||
}
|
||||
messageCount++;
|
||||
}
|
||||
}
|
||||
|
@ -846,7 +846,7 @@ public final class AnalyticsCollectorTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (!(other instanceof EventWindowAndPeriodId)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -15,9 +15,11 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import static com.google.android.exoplayer2.util.UriUtil.removeQueryParameter;
|
||||
import static com.google.android.exoplayer2.util.UriUtil.resolve;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
@ -104,4 +106,36 @@ public final class UriUtilTest {
|
||||
assertThat(resolve("a:b", "../c")).isEqualTo("a:c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeOnlyQueryParameter() {
|
||||
Uri uri = Uri.parse("http://uri?query=value");
|
||||
assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeFirstQueryParameter() {
|
||||
Uri uri = Uri.parse("http://uri?query=value&second=value2");
|
||||
assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?second=value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeMiddleQueryParameter() {
|
||||
Uri uri = Uri.parse("http://uri?first=value1&query=value&last=value2");
|
||||
assertThat(removeQueryParameter(uri, "query").toString())
|
||||
.isEqualTo("http://uri?first=value1&last=value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeLastQueryParameter() {
|
||||
Uri uri = Uri.parse("http://uri?first=value1&query=value");
|
||||
assertThat(removeQueryParameter(uri, "query").toString()).isEqualTo("http://uri?first=value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeNonExistentQueryParameter() {
|
||||
Uri uri = Uri.parse("http://uri");
|
||||
assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri");
|
||||
uri = Uri.parse("http://uri?query=value");
|
||||
assertThat(removeQueryParameter(uri, "foo").toString()).isEqualTo("http://uri?query=value");
|
||||
}
|
||||
}
|
||||
|
@ -30,15 +30,11 @@ android {
|
||||
// testCoverageEnabled = true
|
||||
// }
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("../../checker-framework-lint.xml")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -56,6 +56,12 @@ public final class DashDownloadHelper extends DownloadHelper {
|
||||
manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri);
|
||||
}
|
||||
|
||||
/** Returns the DASH manifest. Must not be called until after preparation completes. */
|
||||
public DashManifest getManifest() {
|
||||
Assertions.checkNotNull(manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
Assertions.checkNotNull(manifest);
|
||||
|
@ -30,15 +30,11 @@ android {
|
||||
// testCoverageEnabled = true
|
||||
// }
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("../../checker-framework-lint.xml")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -198,24 +198,24 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Returns the next chunk to load.
|
||||
* <p>
|
||||
* If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has
|
||||
* been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but
|
||||
* the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
|
||||
*
|
||||
* <p>If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream
|
||||
* has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available
|
||||
* but the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
|
||||
* contain the {@link HlsUrl} that refers to the playlist that needs refreshing.
|
||||
*
|
||||
* @param previous The most recently loaded media chunk.
|
||||
* @param playbackPositionUs The current playback position in microseconds. If playback of the
|
||||
* period to which this chunk source belongs has not yet started, the value will be the
|
||||
* starting position in the period minus the duration of any media in previous periods still
|
||||
* to be played.
|
||||
* @param loadPositionUs The current load position in microseconds. If {@code previous} is null,
|
||||
* this is the starting position from which chunks should be provided. Else it's equal to
|
||||
* {@code previous.endTimeUs}.
|
||||
* @param playbackPositionUs The current playback position relative to the period start in
|
||||
* microseconds. If playback of the period to which this chunk source belongs has not yet
|
||||
* started, the value will be the starting position in the period minus the duration of any
|
||||
* media in previous periods still to be played.
|
||||
* @param loadPositionUs The current load position relative to the period start in microseconds.
|
||||
* If {@code previous} is null, this is the starting position from which chunks should be
|
||||
* provided. Else it's equal to {@code previous.endTimeUs}.
|
||||
* @param out A holder to populate.
|
||||
*/
|
||||
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs,
|
||||
HlsChunkHolder out) {
|
||||
public void getNextChunk(
|
||||
HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, HlsChunkHolder out) {
|
||||
int oldVariantIndex = previous == null ? C.INDEX_UNSET
|
||||
: trackGroup.indexOf(previous.trackFormat);
|
||||
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
|
||||
@ -261,12 +261,13 @@ import java.util.List;
|
||||
// If the playlist is too old to contain the chunk, we need to refresh it.
|
||||
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
||||
} else {
|
||||
// The playlist start time is subtracted from the target position because the segment start
|
||||
// times are relative to the start of the playlist, but the target position is not.
|
||||
long positionOfPlaylistInPeriodUs =
|
||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs;
|
||||
chunkMediaSequence =
|
||||
Util.binarySearchFloor(
|
||||
mediaPlaylist.segments,
|
||||
/* value= */ targetPositionUs - mediaPlaylist.startTimeUs,
|
||||
/* value= */ targetPositionInPlaylistUs,
|
||||
/* inclusive= */ true,
|
||||
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
|
||||
+ mediaPlaylist.mediaSequence;
|
||||
@ -330,9 +331,9 @@ import java.util.List;
|
||||
}
|
||||
|
||||
// Compute start time of the next chunk.
|
||||
long offsetFromInitialStartTimeUs =
|
||||
long positionOfPlaylistInPeriodUs =
|
||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
long startTimeUs = offsetFromInitialStartTimeUs + segment.relativeStartTimeUs;
|
||||
long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
||||
int discontinuitySequence = mediaPlaylist.discontinuitySequence
|
||||
+ segment.relativeDiscontinuitySequence;
|
||||
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
||||
@ -352,8 +353,8 @@ import java.util.List;
|
||||
muxedCaptionFormats,
|
||||
trackSelection.getSelectionReason(),
|
||||
trackSelection.getSelectionData(),
|
||||
startTimeUs,
|
||||
startTimeUs + segment.durationUs,
|
||||
segmentStartTimeInPeriodUs,
|
||||
segmentStartTimeInPeriodUs + segment.durationUs,
|
||||
chunkMediaSequence,
|
||||
discontinuitySequence,
|
||||
segment.hasGapTag,
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
@ -36,6 +37,11 @@ import java.io.IOException;
|
||||
sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
|
||||
}
|
||||
|
||||
public void bindSampleQueue() {
|
||||
Assertions.checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);
|
||||
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
|
||||
}
|
||||
|
||||
public void unbindSampleQueue() {
|
||||
if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
|
||||
sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
|
||||
@ -48,12 +54,11 @@ import java.io.IOException;
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
|
||||
|| (maybeMapToSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex));
|
||||
|| (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowError() throws IOException {
|
||||
maybeMapToSampleQueue();
|
||||
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {
|
||||
throw new SampleQueueMappingException(
|
||||
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
|
||||
@ -63,22 +68,21 @@ import java.io.IOException;
|
||||
|
||||
@Override
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
|
||||
return maybeMapToSampleQueue()
|
||||
return hasValidSampleQueueIndex()
|
||||
? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat)
|
||||
: C.RESULT_NOTHING_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int skipData(long positionUs) {
|
||||
return maybeMapToSampleQueue() ? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs) : 0;
|
||||
return hasValidSampleQueueIndex()
|
||||
? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private boolean maybeMapToSampleQueue() {
|
||||
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
|
||||
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
|
||||
}
|
||||
private boolean hasValidSampleQueueIndex() {
|
||||
return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING
|
||||
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
|
||||
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
|
||||
|
@ -102,6 +102,7 @@ import java.util.Arrays;
|
||||
private final Runnable maybeFinishPrepareRunnable;
|
||||
private final Runnable onTracksEndedRunnable;
|
||||
private final Handler handler;
|
||||
private final ArrayList<HlsSampleStream> hlsSampleStreams;
|
||||
|
||||
private SampleQueue[] sampleQueues;
|
||||
private int[] sampleQueueTrackIds;
|
||||
@ -166,6 +167,7 @@ import java.util.Arrays;
|
||||
sampleQueueIsAudioVideoFlags = new boolean[0];
|
||||
sampleQueuesEnabledStates = new boolean[0];
|
||||
mediaChunks = new ArrayList<>();
|
||||
hlsSampleStreams = new ArrayList<>();
|
||||
maybeFinishPrepareRunnable =
|
||||
new Runnable() {
|
||||
@Override
|
||||
@ -219,9 +221,6 @@ import java.util.Arrays;
|
||||
}
|
||||
|
||||
public int bindSampleQueueToSampleStream(int trackGroupIndex) {
|
||||
if (trackGroupToSampleQueueIndex == null) {
|
||||
return SAMPLE_QUEUE_INDEX_PENDING;
|
||||
}
|
||||
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
|
||||
if (sampleQueueIndex == C.INDEX_UNSET) {
|
||||
return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET
|
||||
@ -295,6 +294,9 @@ import java.util.Arrays;
|
||||
}
|
||||
streams[i] = new HlsSampleStream(this, trackGroupIndex);
|
||||
streamResetFlags[i] = true;
|
||||
if (trackGroupToSampleQueueIndex != null) {
|
||||
((HlsSampleStream) streams[i]).bindSampleQueue();
|
||||
}
|
||||
// If there's still a chance of avoiding a seek, try and seek within the sample queue.
|
||||
if (sampleQueuesBuilt && !seekRequired) {
|
||||
SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]];
|
||||
@ -360,6 +362,7 @@ import java.util.Arrays;
|
||||
}
|
||||
}
|
||||
|
||||
updateSampleStreams(streams);
|
||||
seenFirstTrackSelection = true;
|
||||
return seekRequired;
|
||||
}
|
||||
@ -411,6 +414,7 @@ import java.util.Arrays;
|
||||
loader.release(this);
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
released = true;
|
||||
hlsSampleStreams.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -750,6 +754,15 @@ import java.util.Arrays;
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void updateSampleStreams(SampleStream[] streams) {
|
||||
hlsSampleStreams.clear();
|
||||
for (SampleStream stream : streams) {
|
||||
if (stream != null) {
|
||||
hlsSampleStreams.add((HlsSampleStream) stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean finishedReadingChunk(HlsMediaChunk chunk) {
|
||||
int chunkUid = chunk.uid;
|
||||
int sampleQueueCount = sampleQueues.length;
|
||||
@ -807,6 +820,9 @@ import java.util.Arrays;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (HlsSampleStream sampleStream : hlsSampleStreams) {
|
||||
sampleStream.bindSampleQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.hls;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.source.SampleQueue;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import java.io.IOException;
|
||||
@ -23,7 +24,7 @@ import java.io.IOException;
|
||||
public final class SampleQueueMappingException extends IOException {
|
||||
|
||||
/** @param mimeType The mime type of the track group whose mapping failed. */
|
||||
public SampleQueueMappingException(String mimeType) {
|
||||
public SampleQueueMappingException(@Nullable String mimeType) {
|
||||
super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + ".");
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,12 @@ public final class HlsDownloadHelper extends DownloadHelper {
|
||||
playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri);
|
||||
}
|
||||
|
||||
/** Returns the HLS playlist. Must not be called until after preparation completes. */
|
||||
public HlsPlaylist getPlaylist() {
|
||||
Assertions.checkNotNull(playlist);
|
||||
return playlist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
Assertions.checkNotNull(playlist);
|
||||
|
@ -146,7 +146,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
|
||||
*/
|
||||
public final long startOffsetUs;
|
||||
/**
|
||||
* The start time of the playlist in playback timebase in microseconds.
|
||||
* If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.
|
||||
* Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
|
||||
* playlist.
|
||||
*/
|
||||
public final long startTimeUs;
|
||||
/**
|
||||
|
@ -208,7 +208,10 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Returns the start time of the first loaded primary playlist. */
|
||||
/**
|
||||
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
|
||||
* media playlist has been loaded.
|
||||
*/
|
||||
public long getInitialStartTimeUs() {
|
||||
return initialStartTimeUs;
|
||||
}
|
||||
@ -567,7 +570,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
|
||||
loadDurationMs, loadable.bytesLoaded(), error, isFatal);
|
||||
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
|
||||
boolean shouldRetryIfNotFatal = notifyPlaylistError(playlistUrl, shouldBlacklist);
|
||||
boolean shouldRetryIfNotFatal =
|
||||
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
|
||||
if (isFatal) {
|
||||
return Loader.DONT_RETRY_FATAL;
|
||||
}
|
||||
|
@ -30,15 +30,11 @@ android {
|
||||
// testCoverageEnabled = true
|
||||
// }
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("../../checker-framework-lint.xml")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -52,6 +52,12 @@ public final class SsDownloadHelper extends DownloadHelper {
|
||||
manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri);
|
||||
}
|
||||
|
||||
/** Returns the SmoothStreaming manifest. Must not be called until after preparation completes. */
|
||||
public SsManifest getManifest() {
|
||||
Assertions.checkNotNull(manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
Assertions.checkNotNull(manifest);
|
||||
|
@ -36,6 +36,7 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -25,6 +25,7 @@ import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
@ -44,87 +45,83 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/**
|
||||
* A time bar that shows a current position, buffered position, duration and ad markers.
|
||||
* <p>
|
||||
* A DefaultTimeBar can be customized by setting attributes, as outlined below.
|
||||
*
|
||||
* <p>A DefaultTimeBar can be customized by setting attributes, as outlined below.
|
||||
*
|
||||
* <h3>Attributes</h3>
|
||||
*
|
||||
* The following attributes can be set on a DefaultTimeBar when used in a layout XML file:
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>{@code bar_height}</b> - Dimension for the height of the time bar.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}</li>
|
||||
* <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code touch_target_height}</b> - Dimension for the height of the area in which touch
|
||||
* interactions with the time bar are handled. If no height is specified, this also determines
|
||||
* the height of the view.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}</li>
|
||||
* <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code ad_marker_width}</b> - Dimension for the width of any ad markers shown on the
|
||||
* bar. Ad markers are superimposed on the time bar to show the times at which ads will play.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}</li>
|
||||
* <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber
|
||||
* handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle
|
||||
* should be shown.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}</li>
|
||||
* <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber
|
||||
* handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}</li>
|
||||
* <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber
|
||||
* handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}</li>
|
||||
* <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code scrubber_drawable}</b> - Optional reference to a drawable to draw for the
|
||||
* scrubber handle. If set, this overrides the default behavior, which is to draw a circle for
|
||||
* the scrubber handle.
|
||||
* </li>
|
||||
* <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media
|
||||
* before the current playback position.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}</li>
|
||||
* <li>Corresponding method: {@link #setPlayedColor(int)}
|
||||
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code scrubber_color}</b> - Color for the scrubber handle.
|
||||
* <ul>
|
||||
* <li>Default: see {@link #getDefaultScrubberColor(int)}</li>
|
||||
* <li>Corresponding method: {@link #setScrubberColor(int)}
|
||||
* <li>Default: see {@link #getDefaultScrubberColor(int)}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current
|
||||
* played position up to the current buffered position.
|
||||
* <ul>
|
||||
* <li>Default: see {@link #getDefaultBufferedColor(int)}</li>
|
||||
* <li>Corresponding method: {@link #setBufferedColor(int)}
|
||||
* <li>Default: see {@link #getDefaultBufferedColor(int)}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current
|
||||
* buffered position.
|
||||
* <ul>
|
||||
* <li>Default: see {@link #getDefaultUnplayedColor(int)}</li>
|
||||
* <li>Corresponding method: {@link #setUnplayedColor(int)}
|
||||
* <li>Default: see {@link #getDefaultUnplayedColor(int)}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers.
|
||||
* <ul>
|
||||
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}</li>
|
||||
* <li>Corresponding method: {@link #setAdMarkerColor(int)}
|
||||
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers.
|
||||
* <ul>
|
||||
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)}</li>
|
||||
* <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)}
|
||||
* <li>Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class DefaultTimeBar extends View implements TimeBar {
|
||||
@ -324,6 +321,72 @@ public class DefaultTimeBar extends View implements TimeBar {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for the portion of the time bar representing media before the playback position.
|
||||
*
|
||||
* @param playedColor The color for the portion of the time bar representing media before the
|
||||
* playback position.
|
||||
*/
|
||||
public void setPlayedColor(@ColorInt int playedColor) {
|
||||
playedPaint.setColor(playedColor);
|
||||
invalidate(seekBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for the scrubber handle.
|
||||
*
|
||||
* @param scrubberColor The color for the scrubber handle.
|
||||
*/
|
||||
public void setScrubberColor(@ColorInt int scrubberColor) {
|
||||
scrubberPaint.setColor(scrubberColor);
|
||||
invalidate(seekBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for the portion of the time bar after the current played position up to the
|
||||
* current buffered position.
|
||||
*
|
||||
* @param bufferedColor The color for the portion of the time bar after the current played
|
||||
* position up to the current buffered position.
|
||||
*/
|
||||
public void setBufferedColor(@ColorInt int bufferedColor) {
|
||||
bufferedPaint.setColor(bufferedColor);
|
||||
invalidate(seekBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for the portion of the time bar after the current played position.
|
||||
*
|
||||
* @param unplayedColor The color for the portion of the time bar after the current played
|
||||
* position.
|
||||
*/
|
||||
public void setUnplayedColor(@ColorInt int unplayedColor) {
|
||||
unplayedPaint.setColor(unplayedColor);
|
||||
invalidate(seekBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for unplayed ad markers.
|
||||
*
|
||||
* @param adMarkerColor The color for unplayed ad markers.
|
||||
*/
|
||||
public void setAdMarkerColor(@ColorInt int adMarkerColor) {
|
||||
adMarkerPaint.setColor(adMarkerColor);
|
||||
invalidate(seekBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for played ad markers.
|
||||
*
|
||||
* @param playedAdMarkerColor The color for played ad markers.
|
||||
*/
|
||||
public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) {
|
||||
playedAdMarkerPaint.setColor(playedAdMarkerColor);
|
||||
invalidate(seekBounds);
|
||||
}
|
||||
|
||||
// TimeBar implementation.
|
||||
|
||||
@Override
|
||||
public void addListener(OnScrubListener listener) {
|
||||
listeners.add(listener);
|
||||
@ -381,6 +444,8 @@ public class DefaultTimeBar extends View implements TimeBar {
|
||||
update();
|
||||
}
|
||||
|
||||
// View methods.
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
@ -408,8 +473,8 @@ public class DefaultTimeBar extends View implements TimeBar {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (isInSeekBar(x, y)) {
|
||||
startScrubbing();
|
||||
positionScrubber(x);
|
||||
startScrubbing();
|
||||
scrubPosition = getScrubberPosition();
|
||||
update();
|
||||
invalidate();
|
||||
|
@ -50,6 +50,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* A notification manager to start, update and cancel a media style notification reflecting the
|
||||
@ -205,7 +206,9 @@ public class PlayerNotificationManager {
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (notificationTag == currentNotificationTag && isNotificationStarted) {
|
||||
if (player != null
|
||||
&& notificationTag == currentNotificationTag
|
||||
&& isNotificationStarted) {
|
||||
updateNotification(bitmap);
|
||||
}
|
||||
}
|
||||
@ -260,7 +263,7 @@ public class PlayerNotificationManager {
|
||||
private final String channelId;
|
||||
private final int notificationId;
|
||||
private final MediaDescriptionAdapter mediaDescriptionAdapter;
|
||||
private final CustomActionReceiver customActionReceiver;
|
||||
private final @Nullable CustomActionReceiver customActionReceiver;
|
||||
private final Handler mainHandler;
|
||||
private final NotificationManagerCompat notificationManager;
|
||||
private final IntentFilter intentFilter;
|
||||
@ -269,12 +272,12 @@ public class PlayerNotificationManager {
|
||||
private final Map<String, NotificationCompat.Action> playbackActions;
|
||||
private final Map<String, NotificationCompat.Action> customActions;
|
||||
|
||||
private Player player;
|
||||
private @Nullable Player player;
|
||||
private ControlDispatcher controlDispatcher;
|
||||
private boolean isNotificationStarted;
|
||||
private int currentNotificationTag;
|
||||
private NotificationListener notificationListener;
|
||||
private MediaSessionCompat.Token mediaSessionToken;
|
||||
private @Nullable NotificationListener notificationListener;
|
||||
private @Nullable MediaSessionCompat.Token mediaSessionToken;
|
||||
private boolean useNavigationActions;
|
||||
private boolean usePlayPauseActions;
|
||||
private @Nullable String stopAction;
|
||||
@ -365,6 +368,20 @@ public class PlayerNotificationManager {
|
||||
playerListener = new PlayerListener();
|
||||
notificationBroadcastReceiver = new NotificationBroadcastReceiver();
|
||||
intentFilter = new IntentFilter();
|
||||
useNavigationActions = true;
|
||||
usePlayPauseActions = true;
|
||||
ongoing = true;
|
||||
colorized = true;
|
||||
useChronometer = true;
|
||||
color = Color.TRANSPARENT;
|
||||
smallIconResourceId = R.drawable.exo_notification_small_icon;
|
||||
defaults = 0;
|
||||
priority = NotificationCompat.PRIORITY_LOW;
|
||||
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
|
||||
rewindMs = DEFAULT_REWIND_MS;
|
||||
stopAction = ACTION_STOP;
|
||||
badgeIconType = NotificationCompat.BADGE_ICON_SMALL;
|
||||
visibility = NotificationCompat.VISIBILITY_PUBLIC;
|
||||
|
||||
// initialize actions
|
||||
playbackActions = createPlaybackActions(context);
|
||||
@ -378,22 +395,7 @@ public class PlayerNotificationManager {
|
||||
for (String action : customActions.keySet()) {
|
||||
intentFilter.addAction(action);
|
||||
}
|
||||
|
||||
setStopAction(ACTION_STOP);
|
||||
|
||||
useNavigationActions = true;
|
||||
usePlayPauseActions = true;
|
||||
ongoing = true;
|
||||
colorized = true;
|
||||
useChronometer = true;
|
||||
color = Color.TRANSPARENT;
|
||||
smallIconResourceId = R.drawable.exo_notification_small_icon;
|
||||
defaults = 0;
|
||||
priority = NotificationCompat.PRIORITY_LOW;
|
||||
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
|
||||
rewindMs = DEFAULT_REWIND_MS;
|
||||
setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL);
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -512,10 +514,9 @@ public class PlayerNotificationManager {
|
||||
}
|
||||
this.stopAction = stopAction;
|
||||
if (ACTION_STOP.equals(stopAction)) {
|
||||
stopPendingIntent = playbackActions.get(ACTION_STOP).actionIntent;
|
||||
stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent;
|
||||
} else if (stopAction != null) {
|
||||
Assertions.checkArgument(customActions.containsKey(stopAction));
|
||||
stopPendingIntent = customActions.get(stopAction).actionIntent;
|
||||
stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent;
|
||||
} else {
|
||||
stopPendingIntent = null;
|
||||
}
|
||||
@ -698,25 +699,28 @@ public class PlayerNotificationManager {
|
||||
maybeUpdateNotification();
|
||||
}
|
||||
|
||||
private Notification updateNotification(Bitmap bitmap) {
|
||||
@RequiresNonNull("player")
|
||||
private Notification updateNotification(@Nullable Bitmap bitmap) {
|
||||
Notification notification = createNotification(player, bitmap);
|
||||
notificationManager.notify(notificationId, notification);
|
||||
return notification;
|
||||
}
|
||||
|
||||
private void startOrUpdateNotification() {
|
||||
Notification notification = updateNotification(null);
|
||||
if (!isNotificationStarted) {
|
||||
isNotificationStarted = true;
|
||||
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
|
||||
if (notificationListener != null) {
|
||||
notificationListener.onNotificationStarted(notificationId, notification);
|
||||
if (player != null) {
|
||||
Notification notification = updateNotification(null);
|
||||
if (!isNotificationStarted) {
|
||||
isNotificationStarted = true;
|
||||
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
|
||||
if (notificationListener != null) {
|
||||
notificationListener.onNotificationStarted(notificationId, notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateNotification() {
|
||||
if (isNotificationStarted) {
|
||||
if (isNotificationStarted && player != null) {
|
||||
updateNotification(null);
|
||||
}
|
||||
}
|
||||
@ -732,64 +736,6 @@ public class PlayerNotificationManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, NotificationCompat.Action> createPlaybackActions(Context context) {
|
||||
Map<String, NotificationCompat.Action> actions = new HashMap<>();
|
||||
Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_PLAY,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_play,
|
||||
context.getString(R.string.exo_controls_play_description),
|
||||
PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_PAUSE,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_pause,
|
||||
context.getString(R.string.exo_controls_pause_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_STOP,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_stop,
|
||||
context.getString(R.string.exo_controls_stop_description),
|
||||
PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_REWIND,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_rewind,
|
||||
context.getString(R.string.exo_controls_rewind_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_FAST_FORWARD,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_fastforward,
|
||||
context.getString(R.string.exo_controls_fastforward_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_PREVIOUS,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_previous,
|
||||
context.getString(R.string.exo_controls_previous_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_NEXT,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_next,
|
||||
context.getString(R.string.exo_controls_next_description),
|
||||
PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the notification given the current player state.
|
||||
*
|
||||
@ -821,7 +767,7 @@ public class PlayerNotificationManager {
|
||||
// Configure stop action (eg. when user dismisses the notification when !isOngoing).
|
||||
boolean useStopAction = stopAction != null && !isPlayingAd;
|
||||
mediaStyle.setShowCancelButton(useStopAction);
|
||||
if (useStopAction) {
|
||||
if (useStopAction && stopPendingIntent != null) {
|
||||
builder.setDeleteIntent(stopPendingIntent);
|
||||
mediaStyle.setCancelButtonIntent(stopPendingIntent);
|
||||
}
|
||||
@ -905,7 +851,7 @@ public class PlayerNotificationManager {
|
||||
if (useNavigationActions && player.getNextWindowIndex() != C.INDEX_UNSET) {
|
||||
stringActions.add(ACTION_NEXT);
|
||||
}
|
||||
if (!customActions.isEmpty()) {
|
||||
if (customActionReceiver != null) {
|
||||
stringActions.addAll(customActionReceiver.getCustomActions(player));
|
||||
}
|
||||
if (ACTION_STOP.equals(stopAction)) {
|
||||
@ -932,6 +878,64 @@ public class PlayerNotificationManager {
|
||||
return new int[] {actionIndex};
|
||||
}
|
||||
|
||||
private static Map<String, NotificationCompat.Action> createPlaybackActions(Context context) {
|
||||
Map<String, NotificationCompat.Action> actions = new HashMap<>();
|
||||
Intent playIntent = new Intent(ACTION_PLAY).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_PLAY,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_play,
|
||||
context.getString(R.string.exo_controls_play_description),
|
||||
PendingIntent.getBroadcast(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent pauseIntent = new Intent(ACTION_PAUSE).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_PAUSE,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_pause,
|
||||
context.getString(R.string.exo_controls_pause_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, pauseIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent stopIntent = new Intent(ACTION_STOP).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_STOP,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_stop,
|
||||
context.getString(R.string.exo_controls_stop_description),
|
||||
PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent rewindIntent = new Intent(ACTION_REWIND).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_REWIND,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_rewind,
|
||||
context.getString(R.string.exo_controls_rewind_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, rewindIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent fastForwardIntent = new Intent(ACTION_FAST_FORWARD).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_FAST_FORWARD,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_fastforward,
|
||||
context.getString(R.string.exo_controls_fastforward_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, fastForwardIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent previousIntent = new Intent(ACTION_PREVIOUS).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_PREVIOUS,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_previous,
|
||||
context.getString(R.string.exo_controls_previous_description),
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0, previousIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
Intent nextIntent = new Intent(ACTION_NEXT).setPackage(context.getPackageName());
|
||||
actions.put(
|
||||
ACTION_NEXT,
|
||||
new NotificationCompat.Action(
|
||||
R.drawable.exo_notification_next,
|
||||
context.getString(R.string.exo_controls_next_description),
|
||||
PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_CANCEL_CURRENT)));
|
||||
return actions;
|
||||
}
|
||||
|
||||
private class PlayerListener extends Player.DefaultEventListener {
|
||||
|
||||
@Override
|
||||
@ -946,7 +950,7 @@ public class PlayerNotificationManager {
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
return;
|
||||
}
|
||||
startOrUpdateNotification();
|
||||
@ -954,7 +958,7 @@ public class PlayerNotificationManager {
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
return;
|
||||
}
|
||||
startOrUpdateNotification();
|
||||
@ -967,7 +971,7 @@ public class PlayerNotificationManager {
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int repeatMode) {
|
||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
return;
|
||||
}
|
||||
startOrUpdateNotification();
|
||||
@ -985,7 +989,8 @@ public class PlayerNotificationManager {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!isNotificationStarted) {
|
||||
Player player = PlayerNotificationManager.this.player;
|
||||
if (player == null || !isNotificationStarted) {
|
||||
return;
|
||||
}
|
||||
String action = intent.getAction();
|
||||
@ -1013,7 +1018,7 @@ public class PlayerNotificationManager {
|
||||
} else if (ACTION_STOP.equals(action)) {
|
||||
controlDispatcher.dispatchStop(player, true);
|
||||
stopNotification();
|
||||
} else if (customActions.containsKey(action)) {
|
||||
} else if (customActionReceiver != null && customActions.containsKey(action)) {
|
||||
customActionReceiver.onCustomAction(player, action, intent);
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +133,12 @@ import java.util.List;
|
||||
* <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
|
||||
* <li>Default: {@code unset}
|
||||
* </ul>
|
||||
* <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame
|
||||
* or media artwork is kept visible when the player is reset.
|
||||
* <ul>
|
||||
* <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
|
||||
* <li>Default: {@code false}
|
||||
* </ul>
|
||||
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
|
||||
* for more details.
|
||||
* <ul>
|
||||
@ -242,6 +248,7 @@ public class PlayerView extends FrameLayout {
|
||||
private boolean useArtwork;
|
||||
private Bitmap defaultArtwork;
|
||||
private boolean showBuffering;
|
||||
private boolean keepContentOnPlayerReset;
|
||||
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||
private @Nullable CharSequence customErrorMessage;
|
||||
private int controllerShowTimeoutMs;
|
||||
@ -313,6 +320,9 @@ public class PlayerView extends FrameLayout {
|
||||
a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);
|
||||
controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);
|
||||
showBuffering = a.getBoolean(R.styleable.PlayerView_show_buffering, showBuffering);
|
||||
keepContentOnPlayerReset =
|
||||
a.getBoolean(
|
||||
R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
|
||||
controllerHideDuringAds =
|
||||
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
|
||||
} finally {
|
||||
@ -472,14 +482,12 @@ public class PlayerView extends FrameLayout {
|
||||
if (useController) {
|
||||
controller.setPlayer(player);
|
||||
}
|
||||
if (shutterView != null) {
|
||||
shutterView.setVisibility(VISIBLE);
|
||||
}
|
||||
if (subtitleView != null) {
|
||||
subtitleView.setCues(null);
|
||||
}
|
||||
updateBuffering();
|
||||
updateErrorMessage();
|
||||
updateForCurrentTrackSelections(/* isNewPlayer= */ true);
|
||||
if (player != null) {
|
||||
Player.VideoComponent newVideoComponent = player.getVideoComponent();
|
||||
if (newVideoComponent != null) {
|
||||
@ -496,10 +504,8 @@ public class PlayerView extends FrameLayout {
|
||||
}
|
||||
player.addListener(componentListener);
|
||||
maybeShowController(false);
|
||||
updateForCurrentTrackSelections();
|
||||
} else {
|
||||
hideController();
|
||||
hideArtwork();
|
||||
}
|
||||
}
|
||||
|
||||
@ -542,7 +548,7 @@ public class PlayerView extends FrameLayout {
|
||||
Assertions.checkState(!useArtwork || artworkView != null);
|
||||
if (this.useArtwork != useArtwork) {
|
||||
this.useArtwork = useArtwork;
|
||||
updateForCurrentTrackSelections();
|
||||
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -560,7 +566,7 @@ public class PlayerView extends FrameLayout {
|
||||
public void setDefaultArtwork(Bitmap defaultArtwork) {
|
||||
if (this.defaultArtwork != defaultArtwork) {
|
||||
this.defaultArtwork = defaultArtwork;
|
||||
updateForCurrentTrackSelections();
|
||||
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -600,6 +606,32 @@ public class PlayerView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the currently displayed video frame or media artwork is kept visible when the
|
||||
* player is reset. A player reset is defined to mean the player being re-prepared with different
|
||||
* media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being
|
||||
* replaced or cleared by calling {@link #setPlayer(Player)}.
|
||||
*
|
||||
* <p>If enabled, the currently displayed video frame or media artwork will be kept visible until
|
||||
* the player set on the view has been successfully prepared with new media and loaded enough of
|
||||
* it to have determined the available tracks. Hence enabling this option allows transitioning
|
||||
* from playing one piece of media to another, or from using one player instance to another,
|
||||
* without clearing the view's content.
|
||||
*
|
||||
* <p>If disabled, the currently displayed video frame or media artwork will be hidden as soon as
|
||||
* the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible.
|
||||
* Hence the video frame will not be hidden if using a custom layout that omits this view.
|
||||
*
|
||||
* @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is
|
||||
* kept visible when the player is reset.
|
||||
*/
|
||||
public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
|
||||
if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) {
|
||||
this.keepContentOnPlayerReset = keepContentOnPlayerReset;
|
||||
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The
|
||||
* buffering spinner is not displayed by default.
|
||||
@ -961,10 +993,20 @@ public class PlayerView extends FrameLayout {
|
||||
return player != null && player.isPlayingAd() && player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
private void updateForCurrentTrackSelections() {
|
||||
if (player == null) {
|
||||
private void updateForCurrentTrackSelections(boolean isNewPlayer) {
|
||||
if (player == null || player.getCurrentTrackGroups().isEmpty()) {
|
||||
if (!keepContentOnPlayerReset) {
|
||||
hideArtwork();
|
||||
closeShutter();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNewPlayer && !keepContentOnPlayerReset) {
|
||||
// Hide any video from the previous player.
|
||||
closeShutter();
|
||||
}
|
||||
|
||||
TrackSelectionArray selections = player.getCurrentTrackSelections();
|
||||
for (int i = 0; i < selections.length; i++) {
|
||||
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
|
||||
@ -974,10 +1016,9 @@ public class PlayerView extends FrameLayout {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Video disabled so the shutter must be closed.
|
||||
if (shutterView != null) {
|
||||
shutterView.setVisibility(VISIBLE);
|
||||
}
|
||||
closeShutter();
|
||||
// Display artwork if enabled and available, else hide it.
|
||||
if (useArtwork) {
|
||||
for (int i = 0; i < selections.length; i++) {
|
||||
@ -1034,6 +1075,12 @@ public class PlayerView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void closeShutter() {
|
||||
if (shutterView != null) {
|
||||
shutterView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBuffering() {
|
||||
if (bufferingView != null) {
|
||||
boolean showBufferingSpinner =
|
||||
@ -1177,7 +1224,7 @@ public class PlayerView extends FrameLayout {
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
|
||||
updateForCurrentTrackSelections();
|
||||
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
|
||||
}
|
||||
|
||||
// Player.EventListener implementation
|
||||
|
@ -372,12 +372,22 @@ import com.google.android.exoplayer2.util.Util;
|
||||
float previousBottom = layout.getLineTop(0);
|
||||
int lineCount = layout.getLineCount();
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
lineBounds.left = layout.getLineLeft(i) - textPaddingX;
|
||||
lineBounds.right = layout.getLineRight(i) + textPaddingX;
|
||||
float lineTextBoundLeft = layout.getLineLeft(i);
|
||||
float lineTextBoundRight = layout.getLineRight(i);
|
||||
lineBounds.left = lineTextBoundLeft - textPaddingX;
|
||||
lineBounds.right = lineTextBoundRight + textPaddingX;
|
||||
lineBounds.top = previousBottom;
|
||||
lineBounds.bottom = layout.getLineBottom(i);
|
||||
previousBottom = lineBounds.bottom;
|
||||
canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
|
||||
float lineTextWidth = lineTextBoundRight - lineTextBoundLeft;
|
||||
if (lineTextWidth > 0) {
|
||||
// Do not draw a line's background color if it has no text.
|
||||
// For some reason, calculating the width manually is more reliable than
|
||||
// layout.getLineWidth().
|
||||
// Sometimes, lineTextBoundRight == lineTextBoundLeft, and layout.getLineWidth() still
|
||||
// returns non-zero value.
|
||||
canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,14 +51,10 @@ public final class SubtitleView extends View implements TextOutput {
|
||||
*/
|
||||
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
|
||||
|
||||
private static final int FRACTIONAL = 0;
|
||||
private static final int FRACTIONAL_IGNORE_PADDING = 1;
|
||||
private static final int ABSOLUTE = 2;
|
||||
|
||||
private final List<SubtitlePainter> painters;
|
||||
|
||||
private List<Cue> cues;
|
||||
private int textSizeType;
|
||||
private @Cue.TextSizeType int textSizeType;
|
||||
private float textSize;
|
||||
private boolean applyEmbeddedStyles;
|
||||
private boolean applyEmbeddedFontSizes;
|
||||
@ -72,7 +68,7 @@ public final class SubtitleView extends View implements TextOutput {
|
||||
public SubtitleView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
painters = new ArrayList<>();
|
||||
textSizeType = FRACTIONAL;
|
||||
textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
|
||||
textSize = DEFAULT_TEXT_SIZE_FRACTION;
|
||||
applyEmbeddedStyles = true;
|
||||
applyEmbeddedFontSizes = true;
|
||||
@ -120,7 +116,9 @@ public final class SubtitleView extends View implements TextOutput {
|
||||
} else {
|
||||
resources = context.getResources();
|
||||
}
|
||||
setTextSize(ABSOLUTE, TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
|
||||
setTextSize(
|
||||
Cue.TEXT_SIZE_TYPE_ABSOLUTE,
|
||||
TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,10 +152,14 @@ public final class SubtitleView extends View implements TextOutput {
|
||||
* height after the top and bottom padding has been subtracted.
|
||||
*/
|
||||
public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) {
|
||||
setTextSize(ignorePadding ? FRACTIONAL_IGNORE_PADDING : FRACTIONAL, fractionOfHeight);
|
||||
setTextSize(
|
||||
ignorePadding
|
||||
? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING
|
||||
: Cue.TEXT_SIZE_TYPE_FRACTIONAL,
|
||||
fractionOfHeight);
|
||||
}
|
||||
|
||||
private void setTextSize(int textSizeType, float textSize) {
|
||||
private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
|
||||
if (this.textSizeType == textSizeType && this.textSize == textSize) {
|
||||
return;
|
||||
}
|
||||
@ -255,17 +257,61 @@ public final class SubtitleView extends View implements TextOutput {
|
||||
// No space to draw subtitles.
|
||||
return;
|
||||
}
|
||||
int rawViewHeight = rawBottom - rawTop;
|
||||
int viewHeightMinusPadding = bottom - top;
|
||||
|
||||
float textSizePx = textSizeType == ABSOLUTE ? textSize
|
||||
: textSize * (textSizeType == FRACTIONAL ? (bottom - top) : (rawBottom - rawTop));
|
||||
if (textSizePx <= 0) {
|
||||
float defaultViewTextSizePx =
|
||||
resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
|
||||
if (defaultViewTextSizePx <= 0) {
|
||||
// Text has no height.
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
painters.get(i).draw(cues.get(i), applyEmbeddedStyles, applyEmbeddedFontSizes, style,
|
||||
textSizePx, bottomPaddingFraction, canvas, left, top, right, bottom);
|
||||
Cue cue = cues.get(i);
|
||||
float textSizePx =
|
||||
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
|
||||
SubtitlePainter painter = painters.get(i);
|
||||
painter.draw(
|
||||
cue,
|
||||
applyEmbeddedStyles,
|
||||
applyEmbeddedFontSizes,
|
||||
style,
|
||||
textSizePx,
|
||||
bottomPaddingFraction,
|
||||
canvas,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom);
|
||||
}
|
||||
}
|
||||
|
||||
private float resolveTextSizeForCue(
|
||||
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
|
||||
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
|
||||
return defaultViewTextSizePx;
|
||||
}
|
||||
float defaultCueTextSizePx =
|
||||
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
|
||||
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx;
|
||||
}
|
||||
|
||||
private float resolveTextSize(
|
||||
@Cue.TextSizeType int textSizeType,
|
||||
float textSize,
|
||||
int rawViewHeight,
|
||||
int viewHeightMinusPadding) {
|
||||
switch (textSizeType) {
|
||||
case Cue.TEXT_SIZE_TYPE_ABSOLUTE:
|
||||
return textSize;
|
||||
case Cue.TEXT_SIZE_TYPE_FRACTIONAL:
|
||||
return textSize * viewHeightMinusPadding;
|
||||
case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:
|
||||
return textSize * rawViewHeight;
|
||||
case Cue.TYPE_UNSET:
|
||||
default:
|
||||
return Cue.DIMEN_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,8 @@ import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
@ -54,7 +56,7 @@ public class TrackSelectionView extends LinearLayout {
|
||||
private int rendererIndex;
|
||||
private TrackGroupArray trackGroups;
|
||||
private boolean isDisabled;
|
||||
private SelectionOverride override;
|
||||
private @Nullable SelectionOverride override;
|
||||
|
||||
/**
|
||||
* Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
|
||||
@ -100,11 +102,13 @@ public class TrackSelectionView extends LinearLayout {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TrackSelectionView(Context context, AttributeSet attrs) {
|
||||
public TrackSelectionView(Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TrackSelectionView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
@SuppressWarnings("nullness")
|
||||
public TrackSelectionView(
|
||||
Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
TypedArray attributeArray =
|
||||
context
|
||||
@ -152,7 +156,7 @@ public class TrackSelectionView extends LinearLayout {
|
||||
* @param allowAdaptiveSelections Whether adaptive selection is enabled.
|
||||
*/
|
||||
public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {
|
||||
if (!this.allowAdaptiveSelections == allowAdaptiveSelections) {
|
||||
if (this.allowAdaptiveSelections != allowAdaptiveSelections) {
|
||||
this.allowAdaptiveSelections = allowAdaptiveSelections;
|
||||
updateViews();
|
||||
}
|
||||
@ -168,12 +172,14 @@ public class TrackSelectionView extends LinearLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link TrackNameProvider} used to generate the user visible name of each track.
|
||||
* Sets the {@link TrackNameProvider} used to generate the user visible name of each track and
|
||||
* updates the view with track names queried from the specified provider.
|
||||
*
|
||||
* @param trackNameProvider The {@link TrackNameProvider} to use.
|
||||
*/
|
||||
public void setTrackNameProvider(TrackNameProvider trackNameProvider) {
|
||||
this.trackNameProvider = Assertions.checkNotNull(trackNameProvider);
|
||||
updateViews();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,20 +312,20 @@ public class TrackSelectionView extends LinearLayout {
|
||||
override = new SelectionOverride(groupIndex, trackIndex);
|
||||
} else {
|
||||
// An existing override is being modified.
|
||||
boolean isEnabled = ((CheckedTextView) view).isChecked();
|
||||
int overrideLength = override.length;
|
||||
if (isEnabled) {
|
||||
int[] overrideTracks = override.tracks;
|
||||
if (((CheckedTextView) view).isChecked()) {
|
||||
// Remove the track from the override.
|
||||
if (overrideLength == 1) {
|
||||
// The last track is being removed, so the override becomes empty.
|
||||
override = null;
|
||||
isDisabled = true;
|
||||
} else {
|
||||
int[] tracks = getTracksRemoving(override.tracks, trackIndex);
|
||||
int[] tracks = getTracksRemoving(overrideTracks, trackIndex);
|
||||
override = new SelectionOverride(groupIndex, tracks);
|
||||
}
|
||||
} else {
|
||||
int[] tracks = getTracksAdding(override.tracks, trackIndex);
|
||||
int[] tracks = getTracksAdding(overrideTracks, trackIndex);
|
||||
override = new SelectionOverride(groupIndex, tracks);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
<string name="exo_track_selection_title_video">Video</string>
|
||||
<string name="exo_track_selection_title_audio">Audio</string>
|
||||
<string name="exo_track_selection_title_text">Text</string>
|
||||
<string name="exo_track_selection_none">Keiner</string>
|
||||
<string name="exo_track_selection_none">Ohne</string>
|
||||
<string name="exo_track_selection_auto">Automatisch</string>
|
||||
<string name="exo_track_unknown">Unbekannt</string>
|
||||
<string name="exo_track_resolution">%1$d × %2$d</string>
|
||||
@ -31,5 +31,5 @@
|
||||
<string name="exo_track_surround_5_point_1">5.1-Surround-Sound</string>
|
||||
<string name="exo_track_surround_7_point_1">7.1-Surround-Sound</string>
|
||||
<string name="exo_track_bitrate">%1$.2f Mbit/s</string>
|
||||
<string name="exo_item_list">%1$s und %2$s</string>
|
||||
<string name="exo_item_list">%1$s, %2$s</string>
|
||||
</resources>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user