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