Merge branch 'dev-v2' into dev-v2-8435-bolditalic

This commit is contained in:
Ian Baker 2021-03-04 09:41:39 +00:00 committed by GitHub
commit d80d548503
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
120 changed files with 2271 additions and 1114 deletions

1
.gitignore vendored
View File

@ -47,6 +47,7 @@ bazel-testlogs
.DS_Store
cmake-build-debug
dist
jacoco.exec
tmp
# External native builds

View File

@ -2,22 +2,29 @@
### dev-v2 (not yet released)
* Extractors:
* Add support for MP4 and QuickTime meta atoms that are not full atoms.
* UI:
* Add builder for `PlayerNotificationManager`.
* Add group setting to `PlayerNotificationManager`.
* Audio:
* Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases
([#8585](https://github.com/google/ExoPlayer/issues/8585)).
* Report unexpected discontinuities in
`AnalyticsListener.onAudioSinkError`
([#6384](https://github.com/google/ExoPlayer/issues/6384)).
* Allow forcing offload for gapless content even if gapless playback is
not supported.
* Allow fall back from DTS-HD to DTS when playing via passthrough.
* Analytics:
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
* Downloads and caching:
* Fix `CacheWriter` to correctly handle `DataSource.close` failures, for
which it cannot be assumed that data was successfully written to the
cache.
* Library restructuring:
* `DebugTextViewHelper` moved from `ui` package to `util` package.
* Spherical UI components moved from `video.spherical` package to
`ui.spherical` package, and made package private.
* Core
* Move `getRendererCount` and `getRendererType` methods from `Player` to
`ExoPlayer`.
* Remove deprecated symbols:
* Remove `Player.DefaultEventListener`. Use `Player.EventListener`
instead.
@ -25,6 +32,33 @@
instead.
* Remove `extension-jobdispatcher` module. Use the `extension-workmanager`
module instead.
* DRM:
* Only dispatch DRM session acquire and release events once per period
when playing content that uses the same encryption keys for both audio &
video tracks (previously separate acquire and release events were
dispatched for each track in each period).
* Include the session state in DRM session-acquired listener methods.
* UI
* Fix `StyledPlayerView` scrubber not reappearing correctly in some cases
([#8646](https://github.com/google/ExoPlayer/issues/8646)).
* MediaSession extension: Remove dependency to core module and rely on common
only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for
this purpose and does not rely on the `ConcatenatingMediaSource` anymore.
### 2.13.2 (2021-02-25)
* Extractors:
* Add support for MP4 and QuickTime meta atoms that are not full atoms.
* UI:
* Make conditions to enable UI actions consistent in
`DefaultControlDispatcher`, `PlayerControlView`,
`StyledPlayerControlView`, `PlayerNotificationManager` and
`TimelineQueueNavigator`.
* Fix conditions to enable seeking to next/previous media item to handle
the case where a live stream has ended.
* Audio:
* Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases
([#8585](https://github.com/google/ExoPlayer/issues/8585)).
* IMA extension:
* Fix a bug where playback could get stuck when seeking into a playlist
item with ads, if the preroll ad had preloaded but the window position
@ -32,13 +66,16 @@
* Fix a bug with playback of ads in playlists, where the incorrect period
index was used when deciding whether to trigger playback of an ad after
a seek.
* VP9 extension: Update to use NDK r22
* Text:
* Parse SSA/ASS font size in `Style:` lines
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
* VP9 extension: Update to use NDK r21
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
* FLAC extension: Update to use NDK r22
* FLAC extension: Update to use NDK r21
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
* Opus extension: Update to use NDK r22
* Opus extension: Update to use NDK r21
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
* FFmpeg extension: Update to use NDK r22
* FFmpeg extension: Update to use NDK r21
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
### 2.13.1 (2021-02-12)

View File

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.13.1'
releaseVersionCode = 2013001
releaseVersion = '2.13.2'
releaseVersionCode = 2013002
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.

View File

@ -38,6 +38,7 @@ android {
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true

View File

@ -31,7 +31,8 @@
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop" android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
android:theme="@style/Theme.AppCompat"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -34,6 +34,7 @@ android {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
}
}

View File

@ -38,6 +38,7 @@ android {
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true

View File

@ -41,7 +41,8 @@
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden"
android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
android:theme="@style/Theme.AppCompat"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -65,7 +66,8 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:label="@string/application_name"
android:theme="@style/PlayerTheme">
android:theme="@style/PlayerTheme"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.exoplayer.demo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>

View File

@ -34,6 +34,7 @@ android {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
}
}

View File

@ -21,7 +21,8 @@
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name">
android:label="@string/application_name"
android:exported="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

View File

@ -484,26 +484,6 @@ public final class CastPlayer extends BasePlayer {
sessionManager.endCurrentSession(false);
}
@Override
public int getRendererCount() {
// We assume there are three renderers: video, audio, and text.
return RENDERER_COUNT;
}
@Override
public int getRendererType(int index) {
switch (index) {
case RENDERER_INDEX_VIDEO:
return C.TRACK_TYPE_VIDEO;
case RENDERER_INDEX_AUDIO:
return C.TRACK_TYPE_AUDIO;
case RENDERER_INDEX_TEXT:
return C.TRACK_TYPE_TEXT;
default:
throw new IndexOutOfBoundsException();
}
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) {
@ -708,15 +688,19 @@ public final class CastPlayer extends BasePlayer {
}
}
@SuppressWarnings("deprecation") // Calling deprecated listener method.
private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) {
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
Timeline timeline = currentTimeline;
listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED,
listener ->
listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
listener -> {
listener.onTimelineChanged(
timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
});
}
}

View File

@ -321,7 +321,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Accessed by the calling thread only.
private boolean opened;
private long bytesToSkip;
private long bytesRemaining;
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
@ -577,7 +576,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
byte[] responseBody;
try {
responseBody = readResponseBody();
} catch (HttpDataSourceException e) {
} catch (IOException e) {
responseBody = Util.EMPTY_BYTE_ARRAY;
}
@ -607,7 +606,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Calculate the content length.
if (!isCompressed(responseInfo)) {
@ -627,6 +626,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
opened = true;
transferStarted(dataSpec);
try {
if (!skipFully(bytesToSkip)) {
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
} catch (IOException e) {
throw new OpenException(e, dataSpec, Status.READING_RESPONSE);
}
return bytesRemaining;
}
@ -641,25 +648,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
ByteBuffer readBuffer = getOrCreateReadBuffer();
while (!readBuffer.hasRemaining()) {
if (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
readInternal(readBuffer);
try {
readInternal(readBuffer);
} catch (IOException e) {
throw new HttpDataSourceException(
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
}
if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
if (bytesToSkip > 0) {
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
readBuffer.position(readBuffer.position() + bytesSkipped);
bytesToSkip -= bytesSkipped;
}
}
// The operation didn't time out, fail or finish, and therefore data must have been read.
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
}
// Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
@ -718,17 +725,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
int readLength = buffer.remaining();
if (readBuffer != null) {
// Skip all the bytes we can from readBuffer if there are still bytes to skip.
if (bytesToSkip != 0) {
if (bytesToSkip >= readBuffer.remaining()) {
bytesToSkip -= readBuffer.remaining();
readBuffer.position(readBuffer.limit());
} else {
readBuffer.position(readBuffer.position() + (int) bytesToSkip);
bytesToSkip = 0;
}
}
// If there is existing data in the readBuffer, read as much as possible. Return if any read.
int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
if (copyBytes != 0) {
@ -740,44 +736,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
}
boolean readMore = true;
while (readMore) {
// If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
// buffer. If we do not need to skip bytes, we may write to buffer directly.
final boolean useCallerBuffer = bytesToSkip == 0;
operation.close();
if (!useCallerBuffer) {
ByteBuffer readBuffer = getOrCreateReadBuffer();
readBuffer.clear();
if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
readBuffer.limit((int) bytesToSkip);
}
}
// Fill buffer with more data from Cronet.
readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.
Assertions.checkState(
useCallerBuffer
? readLength > buffer.remaining()
: castNonNull(readBuffer).position() > 0);
// If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
if (useCallerBuffer) {
readMore = false;
} else {
bytesToSkip -= castNonNull(readBuffer).position();
}
}
// Fill buffer with more data from Cronet.
operation.close();
try {
readInternal(buffer);
} catch (IOException e) {
throw new HttpDataSourceException(
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
}
final int bytesRead = readLength - buffer.remaining();
if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
}
// The operation didn't time out, fail or finish, and therefore data must have been read.
Assertions.checkState(readLength > buffer.remaining());
int bytesRead = readLength - buffer.remaining();
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
@ -885,13 +860,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
/**
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws IOException If an error occurs reading from the source.
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
*/
private boolean skipFully(long bytesToSkip) throws IOException {
if (bytesToSkip == 0) {
return true;
}
ByteBuffer readBuffer = getOrCreateReadBuffer();
while (bytesToSkip > 0) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
readInternal(readBuffer);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (finished) {
return false;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
readBuffer.position(readBuffer.position() + bytesSkipped);
bytesToSkip -= bytesSkipped;
}
}
return true;
}
/**
* Reads the whole response body.
*
* @return The response body.
* @throws HttpDataSourceException If an error occurs reading from the source.
* @throws IOException If an error occurs reading from the source.
*/
private byte[] readResponseBody() throws HttpDataSourceException {
private byte[] readResponseBody() throws IOException {
byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
ByteBuffer readBuffer = getOrCreateReadBuffer();
while (!finished) {
@ -914,10 +925,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* the current {@code readBuffer} object so that it is not reused in the future.
*
* @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
* @throws HttpDataSourceException If an error occurs reading from the source.
* @throws IOException If an error occurs reading from the source.
*/
@SuppressWarnings("ReferenceEquality")
private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
private void readInternal(ByteBuffer buffer) throws IOException {
castNonNull(currentUrlRequest).read(buffer);
try {
if (!operation.block(readTimeoutMs)) {
@ -930,23 +941,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
readBuffer = null;
}
Thread.currentThread().interrupt();
throw new HttpDataSourceException(
new InterruptedIOException(),
castNonNull(currentDataSpec),
HttpDataSourceException.TYPE_READ);
throw new InterruptedIOException();
} catch (SocketTimeoutException e) {
// The operation is ongoing so replace buffer to avoid it being written to by this
// operation during a subsequent request.
if (buffer == readBuffer) {
readBuffer = null;
}
throw new HttpDataSourceException(
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
throw e;
}
if (exception != null) {
throw new HttpDataSourceException(
exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
throw exception;
}
}

View File

@ -256,6 +256,7 @@ public final class CronetDataSourceTest {
public void requestSetsRangeHeader() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
mockResponseStartSuccess();
mockReadSuccess(0, 1000);
dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1.
@ -287,8 +288,6 @@ public final class CronetDataSourceTest {
testDataSpec =
new DataSpec.Builder()
.setUri(TEST_URL)
.setPosition(1000)
.setLength(5000)
.setHttpRequestHeaders(dataSpecRequestProperties)
.build();
mockResponseStartSuccess();
@ -1198,6 +1197,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
mockSingleRedirectSuccess();
mockReadSuccess(0, 1000);
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
@ -1368,7 +1368,7 @@ public final class CronetDataSourceTest {
@Test
public void allowDirectExecutor() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
testDataSpec = new DataSpec(Uri.parse(TEST_URL));
mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec);

View File

@ -30,7 +30,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
```
* Download the [Android NDK][] and set its location in a shell variable.
This build configuration has been tested on NDK r22.
This build configuration has been tested on NDK r21.
```
NDK_PATH="<path to Android NDK>"

View File

@ -29,7 +29,7 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
This build configuration has been tested on NDK r22.
This build configuration has been tested on NDK r21.
```
NDK_PATH="<path to Android NDK>"

View File

@ -60,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener;
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@ -700,12 +701,7 @@ import java.util.Map;
// Check for a selected track using an audio renderer.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
return 100;
}
}
return 0;
return TrackSelectionUtil.hasTrackOfType(trackSelections, C.TRACK_TYPE_AUDIO) ? 100 : 0;
}
private void handleAdEvent(AdEvent adEvent) {

View File

@ -13,8 +13,6 @@
// limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android.defaultConfig.minSdkVersion 19
dependencies {
implementation project(modulePrefix + 'library-common')
implementation 'androidx.collection:collection:' + androidxCollectionVersion

View File

@ -28,8 +28,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
@ -43,7 +41,6 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.ControlDispatcher;
@ -93,7 +90,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_onceWithAudioResource_changesPlayerStateToPlaying() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
@ -120,7 +116,6 @@ public class SessionPlayerConnectorTest {
@Test
@MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_onceWithAudioResourceOnMainThread_notifiesOnPlayerStateChanged()
throws Exception {
CountDownLatch onPlayerStatePlayingLatch = new CountDownLatch(1);
@ -158,7 +153,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_withCustomControlDispatcher_isSkipped() throws Exception {
if (Looper.myLooper() == null) {
Looper.prepare();
@ -194,7 +188,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setMediaItem_withAudioResource_notifiesOnPlaybackCompleted() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
@ -219,7 +212,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setMediaItem_withVideoResource_notifiesOnPlaybackCompleted() throws Exception {
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
CountDownLatch onPlaybackCompletedLatch = new CountDownLatch(1);
@ -243,7 +235,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getDuration_whenIdleState_returnsUnknownTime() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getDuration()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
@ -251,7 +242,6 @@ public class SessionPlayerConnectorTest {
@Test
@MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getDuration_afterPrepared_returnsDuration() throws Exception {
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
@ -263,7 +253,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getCurrentPosition_whenIdleState_returnsDefaultPosition() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
@ -271,7 +260,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getBufferedPosition_whenIdleState_returnsDefaultPosition() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0);
@ -279,7 +267,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getPlaybackSpeed_whenIdleState_throwsNoException() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
try {
@ -291,7 +278,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_withDataSourceCallback_changesPlayerState() throws Exception {
sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem(R.raw.video_big_buck_bunny));
sessionPlayerConnector.prepare();
@ -308,7 +294,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setMediaItem_withNullMediaItem_throwsException() {
try {
sessionPlayerConnector.setMediaItem(null);
@ -320,7 +305,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_afterPlayback_remainsSame() throws Exception {
int resId1 = R.raw.video_big_buck_bunny;
MediaItem mediaItem1 =
@ -363,7 +347,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_withSeriesOfSeek_succeeds() throws Exception {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@ -378,7 +361,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_skipsUnnecessarySeek() throws Exception {
CountDownLatch readAllowedLatch = new CountDownLatch(1);
playerTestRule.setDataSourceInstrumentation(
@ -435,7 +417,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_whenUnderlyingPlayerAlsoSeeks_throwsNoException() throws Exception {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@ -456,7 +437,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_byUnderlyingPlayer_notifiesOnSeekCompleted() throws Exception {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@ -484,7 +464,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getPlayerState_withCallingPrepareAndPlayAndPause_reflectsPlayerState()
throws Throwable {
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
@ -521,7 +500,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = VERSION_CODES.KITKAT)
public void prepare_twice_finishes() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@ -530,7 +508,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void prepare_notifiesOnPlayerStateChanged() throws Throwable {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@ -552,7 +529,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void prepare_notifiesBufferingCompletedOnce() throws Throwable {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@ -587,7 +563,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_whenPrepared_notifiesOnSeekCompleted() throws Throwable {
long mp4DurationMs = 8_484L;
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@ -611,7 +586,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_whenPrepared_notifiesOnPlaybackSpeedChanged() throws Throwable {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@ -636,7 +610,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_withZeroSpeed_throwsException() {
try {
sessionPlayerConnector.setPlaybackSpeed(0.0f);
@ -648,7 +621,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_withNegativeSpeed_throwsException() {
try {
sessionPlayerConnector.setPlaybackSpeed(-1.0f);
@ -660,7 +632,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void close_throwsNoExceptionAndDoesNotCrash() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
AudioAttributesCompat attributes =
@ -679,7 +650,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void cancelReturnedFuture_withSeekTo_cancelsPendingCommand() throws Exception {
CountDownLatch readRequestedLatch = new CountDownLatch(1);
CountDownLatch readAllowedLatch = new CountDownLatch(1);
@ -719,7 +689,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_withNullPlaylist_throwsException() throws Exception {
try {
sessionPlayerConnector.setPlaylist(null, null);
@ -731,7 +700,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_withPlaylistContainingNullItem_throwsException() {
try {
List<MediaItem> list = new ArrayList<>();
@ -745,7 +713,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
@ -760,7 +727,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylistAndRemoveAllPlaylistItem_playerStateBecomesIdle() throws Exception {
List<MediaItem> playlist = new ArrayList<>();
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
@ -786,7 +752,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
@ -811,7 +776,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged()
throws Exception {
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
@ -842,7 +806,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged()
throws Exception {
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
@ -876,7 +839,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void addPlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
@ -905,7 +867,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void removePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
@ -933,7 +894,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void movePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = new ArrayList<>();
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
@ -967,7 +927,6 @@ public class SessionPlayerConnectorTest {
@Ignore
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void replacePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
@ -996,7 +955,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_withPlaylist_notifiesOnCurrentMediaItemChanged() throws Exception {
int listSize = 2;
List<MediaItem> playlist = TestUtils.createPlaylist(listSize);
@ -1011,7 +969,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_twice_finishes() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@ -1021,7 +978,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_withPlaylist_notifiesOnCurrentMediaItemChangedAndOnPlaybackCompleted()
throws Exception {
List<MediaItem> playlist = new ArrayList<>();
@ -1060,7 +1016,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
@ -1086,7 +1041,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void pause_twice_finishes() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@ -1097,7 +1051,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void pause_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
@ -1124,7 +1077,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void pause_byUnderlyingPlayerInListener_changesToPlayerStatePaused() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
@ -1169,7 +1121,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void skipToNextAndPrevious_calledInARow_notifiesOnCurrentMediaItemChanged()
throws Exception {
List<MediaItem> playlist = new ArrayList<>();
@ -1221,7 +1172,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setRepeatMode_withRepeatAll_continuesToPlayPlaylistWithoutBeingCompleted()
throws Exception {
List<MediaItem> playlist = new ArrayList<>();

View File

@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-common')
api 'androidx.media:media:' + androidxMediaVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion

View File

@ -23,15 +23,13 @@ import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
/**
* A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
* ConcatenatingMediaSource}.
* A {@link MediaSessionConnector.QueueEditor} implementation.
*
* <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
@ -44,18 +42,17 @@ public final class TimelineQueueEditor
public static final String EXTRA_FROM_INDEX = "from_index";
public static final String EXTRA_TO_INDEX = "to_index";
/**
* Factory to create {@link MediaSource}s.
*/
public interface MediaSourceFactory {
/** Converts a {@link MediaDescriptionCompat} to a {@link MediaItem}. */
public interface MediaDescriptionConverter {
/**
* Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}.
* Returns a {@link MediaItem} for the given {@link MediaDescriptionCompat} or null if the
* description can't be converted.
*
* @param description The {@link MediaDescriptionCompat} to create a media source for.
* @return A {@link MediaSource} or {@code null} if no source can be created for the given
* description.
* <p>If not null, the media item that is returned will be used to call {@link
* Player#addMediaItem(MediaItem)}.
*/
@Nullable MediaSource createMediaSource(MediaDescriptionCompat description);
@Nullable
MediaItem convert(MediaDescriptionCompat description);
}
/**
@ -110,51 +107,46 @@ public final class TimelineQueueEditor
public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {
return Util.areEqual(d1.getMediaId(), d2.getMediaId());
}
}
private final MediaControllerCompat mediaController;
private final QueueDataAdapter queueDataAdapter;
private final MediaSourceFactory sourceFactory;
private final MediaDescriptionConverter mediaDescriptionConverter;
private final MediaDescriptionEqualityChecker equalityChecker;
private final ConcatenatingMediaSource queueMediaSource;
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
* @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media
* descriptions to {@link MediaItem MediaItems}.
*/
public TimelineQueueEditor(
MediaControllerCompat mediaController,
ConcatenatingMediaSource queueMediaSource,
QueueDataAdapter queueDataAdapter,
MediaSourceFactory sourceFactory) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
new MediaIdEqualityChecker());
MediaDescriptionConverter mediaDescriptionConverter) {
this(
mediaController, queueDataAdapter, mediaDescriptionConverter, new MediaIdEqualityChecker());
}
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
* @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media
* descriptions to {@link MediaItem MediaItems}.
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/
public TimelineQueueEditor(
MediaControllerCompat mediaController,
ConcatenatingMediaSource queueMediaSource,
QueueDataAdapter queueDataAdapter,
MediaSourceFactory sourceFactory,
MediaDescriptionConverter mediaDescriptionConverter,
MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource;
this.queueDataAdapter = queueDataAdapter;
this.sourceFactory = sourceFactory;
this.mediaDescriptionConverter = mediaDescriptionConverter;
this.equalityChecker = equalityChecker;
}
@ -165,10 +157,10 @@ public final class TimelineQueueEditor
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
@Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) {
@Nullable MediaItem mediaItem = mediaDescriptionConverter.convert(description);
if (mediaItem != null) {
queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource);
player.addMediaItem(index, mediaItem);
}
}
@ -178,7 +170,7 @@ public final class TimelineQueueEditor
for (int i = 0; i < queue.size(); i++) {
if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
queueDataAdapter.remove(i);
queueMediaSource.removeMediaSource(i);
player.removeMediaItem(i);
return;
}
}
@ -200,9 +192,8 @@ public final class TimelineQueueEditor
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
queueDataAdapter.move(from, to);
queueMediaSource.moveMediaSource(from, to);
player.moveMediaItem(from, to);
}
return true;
}
}

View File

@ -98,8 +98,10 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
if (!timeline.isEmpty() && !player.isPlayingAd()) {
timeline.getWindow(player.getCurrentWindowIndex(), window);
enableSkipTo = timeline.getWindowCount() > 1;
enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();
enableNext = window.isDynamic || player.hasNext();
enablePrevious = window.isSeekable || !window.isLive() || player.hasPrevious();
enableNext =
(window.isLive() && window.isDynamic)
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
long actions = 0;

View File

@ -168,8 +168,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
}
private static final byte[] SKIP_BUFFER = new byte[4096];
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
@ -183,10 +181,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
@Nullable private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesToRead;
private long bytesRead;
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
@ -332,7 +328,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
if (dataSpec.length != C.LENGTH_UNSET) {
@ -345,13 +341,21 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
opened = true;
transferStarted(dataSpec);
try {
if (!skipFully(bytesToSkip)) {
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
} catch (IOException e) {
closeConnectionQuietly();
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
throw new HttpDataSourceException(
@ -369,8 +373,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* Returns the number of bytes that have been skipped since the most recent call to
* {@link #open(DataSpec)}.
* Returns the number of bytes that were skipped during the most recent call to {@link
* #open(DataSpec)}.
*
* @return The number of bytes skipped.
*/
@ -454,30 +458,32 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* Skips any bytes that need skipping. Else does nothing.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
* @throws IOException If an error occurs reading from the source.
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
*/
private void skipInternal() throws IOException {
if (bytesSkipped == bytesToSkip) {
return;
private boolean skipFully(long bytesToSkip) throws IOException {
if (bytesToSkip == 0) {
return true;
}
byte[] skipBuffer = new byte[4096];
while (bytesSkipped != bytesToSkip) {
int readLength = (int) min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = castNonNull(responseByteStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
return false;
}
bytesSkipped += read;
bytesTransferred(read);
}
return true;
}
/**

View File

@ -29,7 +29,7 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
This build configuration has been tested on NDK r22.
This build configuration has been tested on NDK r21.
```
NDK_PATH="<path to Android NDK>"

View File

@ -29,7 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
This build configuration has been tested on NDK r22.
This build configuration has been tested on NDK r21.
```
NDK_PATH="<path to Android NDK>"

View File

@ -30,44 +30,44 @@ public abstract class BasePlayer implements Player {
}
@Override
public void setMediaItem(MediaItem mediaItem) {
public final void setMediaItem(MediaItem mediaItem) {
setMediaItems(Collections.singletonList(mediaItem));
}
@Override
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
public final void setMediaItem(MediaItem mediaItem, long startPositionMs) {
setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs);
}
@Override
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems) {
public final void setMediaItems(List<MediaItem> mediaItems) {
setMediaItems(mediaItems, /* resetPosition= */ true);
}
@Override
public void addMediaItem(int index, MediaItem mediaItem) {
public final void addMediaItem(int index, MediaItem mediaItem) {
addMediaItems(index, Collections.singletonList(mediaItem));
}
@Override
public void addMediaItem(MediaItem mediaItem) {
public final void addMediaItem(MediaItem mediaItem) {
addMediaItems(Collections.singletonList(mediaItem));
}
@Override
public void moveMediaItem(int currentIndex, int newIndex) {
public final void moveMediaItem(int currentIndex, int newIndex) {
if (currentIndex != newIndex) {
moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex);
}
}
@Override
public void removeMediaItem(int index) {
public final void removeMediaItem(int index) {
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);
}
@ -137,6 +137,11 @@ public abstract class BasePlayer implements Player {
}
}
@Override
public final void setPlaybackSpeed(float speed) {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
}
@Override
public final void stop() {
stop(/* reset= */ false);
@ -188,12 +193,12 @@ public abstract class BasePlayer implements Player {
}
@Override
public int getMediaItemCount() {
public final int getMediaItemCount() {
return getCurrentTimeline().getWindowCount();
}
@Override
public MediaItem getMediaItemAt(int index) {
public final MediaItem getMediaItemAt(int index) {
return getCurrentTimeline().getWindow(index, window).mediaItem;
}

View File

@ -79,11 +79,12 @@ public class DefaultControlDispatcher implements ControlDispatcher {
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
int previousWindowIndex = player.getPreviousWindowIndex();
boolean isUnseekableLiveStream = window.isLive() && !window.isSeekable;
if (previousWindowIndex != C.INDEX_UNSET
&& (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (window.isDynamic && !window.isSeekable))) {
|| isUnseekableLiveStream)) {
player.seekTo(previousWindowIndex, C.TIME_UNSET);
} else {
} else if (!isUnseekableLiveStream) {
player.seekTo(windowIndex, /* positionMs= */ 0);
}
return true;
@ -96,10 +97,11 @@ public class DefaultControlDispatcher implements ControlDispatcher {
return true;
}
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
int nextWindowIndex = player.getNextWindowIndex();
if (nextWindowIndex != C.INDEX_UNSET) {
player.seekTo(nextWindowIndex, C.TIME_UNSET);
} else if (timeline.getWindow(windowIndex, window).isLive()) {
} else if (window.isLive() && window.isDynamic) {
player.seekTo(windowIndex, C.TIME_UNSET);
}
return true;

View File

@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.13.1";
public static final String VERSION = "2.13.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.1";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2";
/**
* The version of the library expressed as an integer, for example 1002003.
@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2013001;
public static final int VERSION_INT = 2013002;
/**
* The default user agent for requests made by the library.

View File

@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@ -937,6 +938,7 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_TARGET_OFFSET_MS,
@ -1148,6 +1150,7 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_START_POSITION_MS,
@ -1254,6 +1257,7 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_MEDIA_ID,

View File

@ -19,6 +19,7 @@ import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -69,10 +70,9 @@ public final class MediaMetadata implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_TITLE,
})
@IntDef({FIELD_TITLE})
private @interface FieldNumber {}
private static final int FIELD_TITLE = 0;

View File

@ -55,11 +55,8 @@ import java.util.List;
* which can be obtained by calling {@link #getCurrentTimeline()}.
* <li>They can provide a {@link TrackGroupArray} defining the currently available tracks, which
* can be obtained by calling {@link #getCurrentTrackGroups()}.
* <li>They contain a number of renderers, each of which is able to render tracks of a single type
* (e.g. audio, video or text). The number of renderers and their respective track types can
* be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
* <li>They can provide a {@link TrackSelectionArray} defining which of the currently available
* tracks are selected to be rendered by each renderer. This can be obtained by calling {@link
* tracks are selected to be rendered. This can be obtained by calling {@link
* #getCurrentTrackSelections()}}.
* </ul>
*/
@ -130,13 +127,17 @@ public interface Player {
void clearAuxEffectInfo();
/**
* Sets the audio volume, with 0 being silence and 1 being unity gain.
* Sets the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
*
* @param audioVolume The audio volume.
* @param audioVolume Linear output gain to apply to all audio channels.
*/
void setVolume(float audioVolume);
/** Returns the audio volume, with 0 being silence and 1 being unity gain. */
/**
* Returns the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
*
* @return The linear gain applied to all audio channels.
*/
float getVolume();
/**
@ -400,30 +401,9 @@ public interface Player {
* @param timeline The latest timeline. Never null, but may be empty.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/
@SuppressWarnings("deprecation")
default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
Object manifest = null;
if (timeline.getWindowCount() == 1) {
// Legacy behavior was to report the manifest for single window timelines only.
Timeline.Window window = new Timeline.Window();
manifest = timeline.getWindow(0, window).manifest;
}
// Call deprecated version.
onTimelineChanged(timeline, manifest, reason);
}
default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {}
/**
* Called when the timeline and/or manifest has been refreshed.
*
* <p>Note that if the timeline has changed then a position discontinuity may also have
* occurred. For example, the current period index may have changed as a result of periods being
* added or removed from the timeline. This will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity(int)}.
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest in case the timeline has a single window only. Always
* null if the timeline has more than a single window.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
* window).manifest} for a given window index.
@ -455,8 +435,10 @@ public interface Player {
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
* @param trackSelections The track selections for each renderer. Never null and always of
* length {@link #getRendererCount()}, but may contain null elements.
* @param trackSelections The selected tracks. Never null, but may contain null elements. A
* concrete implementation may include null elements if it has a fixed number of renderer
* components, wishes to report a TrackSelection for each of them, and has one or more
* renderer components that is not assigned any selected tracks.
*/
default void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
@ -488,10 +470,7 @@ public interface Player {
*
* @param isLoading Whether the source is currently being loaded.
*/
@SuppressWarnings("deprecation")
default void onIsLoadingChanged(boolean isLoading) {
onLoadingChanged(isLoading);
}
default void onIsLoadingChanged(boolean isLoading) {}
/** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */
@Deprecated
@ -1131,6 +1110,7 @@ public interface Player {
* Returns the current {@link State playback state} of the player.
*
* @return The current {@link State playback state}.
* @see EventListener#onPlaybackStateChanged(int)
*/
@State
int getPlaybackState();
@ -1140,6 +1120,7 @@ public interface Player {
* true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
*
* @return The current {@link PlaybackSuppressionReason playback suppression reason}.
* @see EventListener#onPlaybackSuppressionReasonChanged(int)
*/
@PlaybackSuppressionReason
int getPlaybackSuppressionReason();
@ -1156,6 +1137,7 @@ public interface Player {
* </ul>
*
* @return Whether the player is playing.
* @see EventListener#onIsPlayingChanged(boolean)
*/
boolean isPlaying();
@ -1168,6 +1150,7 @@ public interface Player {
* {@link #STATE_IDLE}.
*
* @return The error, or {@code null}.
* @see EventListener#onPlayerError(ExoPlaybackException)
*/
@Nullable
ExoPlaybackException getPlayerError();
@ -1199,6 +1182,7 @@ public interface Player {
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
*
* @return Whether playback will proceed when ready.
* @see EventListener#onPlayWhenReadyChanged(boolean, int)
*/
boolean getPlayWhenReady();
@ -1213,6 +1197,7 @@ public interface Player {
* Returns the current {@link RepeatMode} used for playback.
*
* @return The current repeat mode.
* @see EventListener#onRepeatModeChanged(int)
*/
@RepeatMode
int getRepeatMode();
@ -1224,13 +1209,18 @@ public interface Player {
*/
void setShuffleModeEnabled(boolean shuffleModeEnabled);
/** Returns whether shuffling of windows is enabled. */
/**
* Returns whether shuffling of windows is enabled.
*
* @see EventListener#onShuffleModeEnabledChanged(boolean)
*/
boolean getShuffleModeEnabled();
/**
* Whether the player is currently loading the source.
*
* @return Whether the player is currently loading the source.
* @see EventListener#onIsLoadingChanged(boolean)
*/
boolean isLoading();
@ -1325,6 +1315,18 @@ public interface Player {
*/
void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
/**
* Changes the rate at which playback occurs.
*
* <p>The pitch is not changed.
*
* <p>This is equivalent to {@code setPlaybackParameter(getPlaybackParameter().withSpeed(speed))}.
*
* @param speed The linear factor by which playback will be sped up. Must be higher than 0. 1 is
* normal speed, 2 is twice as fast, 0.5 is half normal speed...
*/
void setPlaybackSpeed(float speed);
/**
* Returns the currently active playback parameters.
*
@ -1359,24 +1361,22 @@ public interface Player {
*/
void release();
/** Returns the number of renderers. */
int getRendererCount();
/**
* Returns the track type that the renderer at a given index handles.
* Returns the available track groups.
*
* <p>For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will
* return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}.
*
* @param index The index of the renderer.
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
* @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
*/
int getRendererType(int index);
/** Returns the available track groups. */
TrackGroupArray getCurrentTrackGroups();
/** Returns the current track selections for each renderer. */
/**
* Returns the current track selections.
*
* <p>A concrete implementation may include null elements if it has a fixed number of renderer
* components, wishes to report a TrackSelection for each of them, and has one or more renderer
* components that is not assigned any selected tracks.
*
* @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
*/
TrackSelectionArray getCurrentTrackSelections();
/**
@ -1389,6 +1389,8 @@ public interface Player {
*
* <p>This metadata is considered static in that it comes from the tracks' declared Formats,
* rather than being timed (or dynamic) metadata, which is represented within a metadata track.
*
* @see EventListener#onStaticMetadataChanged(List)
*/
List<Metadata> getCurrentStaticMetadata();
@ -1398,7 +1400,11 @@ public interface Player {
@Nullable
Object getCurrentManifest();
/** Returns the current {@link Timeline}. Never null, but may be empty. */
/**
* Returns the current {@link Timeline}. Never null, but may be empty.
*
* @see EventListener#onTimelineChanged(Timeline, int)
*/
Timeline getCurrentTimeline();
/** Returns the index of the period currently being played. */
@ -1444,6 +1450,8 @@ public interface Player {
/**
* Returns the media item of the current window in the timeline. May be null if the timeline is
* empty.
*
* @see EventListener#onMediaItemTransition(MediaItem, int)
*/
@Nullable
MediaItem getCurrentMediaItem();

View File

@ -22,6 +22,7 @@ import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.Bundleable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -166,6 +167,7 @@ public final class AudioAttributes implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({FIELD_CONTENT_TYPE, FIELD_FLAGS, FIELD_USAGE, FIELD_ALLOWED_CAPTURE_POLICY})
private @interface FieldNumber {}

View File

@ -85,6 +85,7 @@ public final class DeviceInfo implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({FIELD_PLAYBACK_TYPE, FIELD_MIN_VOLUME, FIELD_MAX_VOLUME})
private @interface FieldNumber {}

View File

@ -41,6 +41,10 @@ public final class DataSourceException extends IOException {
return false;
}
/**
* Indicates that the {@link DataSpec#position starting position} of the request was outside the
* bounds of the data.
*/
public static final int POSITION_OUT_OF_RANGE = 0;
/**
@ -56,5 +60,4 @@ public final class DataSourceException extends IOException {
public DataSourceException(int reason) {
this.reason = reason;
}
}

View File

@ -46,7 +46,6 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
@ -221,14 +220,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
@Nullable private DataSpec dataSpec;
@Nullable private HttpURLConnection connection;
@Nullable private InputStream inputStream;
private byte @MonotonicNonNull [] skipBuffer;
private boolean opened;
private int responseCode;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesToRead;
private long bytesRead;
/** @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */
@ -400,7 +396,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
boolean isCompressed = isCompressed(connection);
@ -432,13 +428,21 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
opened = true;
transferStarted(dataSpec);
try {
if (!skipFully(bytesToSkip)) {
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
} catch (IOException e) {
closeConnectionQuietly();
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
throw new HttpDataSourceException(
@ -480,8 +484,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
}
/**
* Returns the number of bytes that have been skipped since the most recent call to
* {@link #open(DataSpec)}.
* Returns the number of bytes that were skipped during the most recent call to {@link
* #open(DataSpec)}.
*
* @return The number of bytes skipped.
*/
@ -725,22 +729,19 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
}
/**
* Skips any bytes that need skipping. Else does nothing.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
* @throws IOException If an error occurs reading from the source.
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
*/
private void skipInternal() throws IOException {
if (bytesSkipped == bytesToSkip) {
return;
private boolean skipFully(long bytesToSkip) throws IOException {
if (bytesToSkip == 0) {
return true;
}
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
byte[] skipBuffer = new byte[4096];
while (bytesSkipped != bytesToSkip) {
int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
@ -748,11 +749,12 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
return false;
}
bytesSkipped += read;
bytesTransferred(read);
}
return true;
}
/**

View File

@ -138,4 +138,11 @@ public final class CopyOnWriteMultiset<E extends Object> implements Iterable<E>
return elements.iterator();
}
}
/** Returns the number of occurrences of an element in this multiset. */
public int count(E element) {
synchronized (lock) {
return elementCounts.containsKey(element) ? elementCounts.get(element) : 0;
}
}
}

View File

@ -107,4 +107,44 @@ public final class CopyOnWriteMultisetTest {
assertThrows(UnsupportedOperationException.class, () -> elementSet.remove("a string"));
}
@Test
public void count() {
CopyOnWriteMultiset<String> multiset = new CopyOnWriteMultiset<>();
multiset.add("a string");
multiset.add("a string");
assertThat(multiset.count("a string")).isEqualTo(2);
assertThat(multiset.count("another string")).isEqualTo(0);
}
@Test
public void modifyingWhileIteratingElements_succeeds() {
CopyOnWriteMultiset<String> multiset = new CopyOnWriteMultiset<>();
multiset.add("a string");
multiset.add("a string");
multiset.add("another string");
// A traditional collection would throw a ConcurrentModificationException here.
for (String element : multiset) {
multiset.remove(element);
}
assertThat(multiset).isEmpty();
}
@Test
public void modifyingWhileIteratingElementSet_succeeds() {
CopyOnWriteMultiset<String> multiset = new CopyOnWriteMultiset<>();
multiset.add("a string");
multiset.add("a string");
multiset.add("another string");
// A traditional collection would throw a ConcurrentModificationException here.
for (String element : multiset.elementSet()) {
multiset.remove(element);
}
assertThat(multiset).containsExactly("a string");
}
}

View File

@ -17,15 +17,12 @@ package com.google.android.exoplayer2.upstream;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.ContentDataSource.ContentDataSourceException;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
@ -85,36 +82,6 @@ public final class ContentDataSourceTest {
}
}
@Test
public void read_positionPastEndOfContent_throwsEOFException() throws Exception {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, /* pipeMode= */ false);
ContentDataSource dataSource =
new ContentDataSource(ApplicationProvider.getApplicationContext());
DataSpec dataSpec = new DataSpec(contentUri, /* position= */ 1025, C.LENGTH_UNSET);
try {
ContentDataSourceException exception =
assertThrows(ContentDataSourceException.class, () -> dataSource.open(dataSpec));
assertThat(exception).hasCauseThat().isInstanceOf(EOFException.class);
} finally {
dataSource.close();
}
}
@Test
public void readPipeMode_positionPastEndOfContent_throwsEOFException() throws Exception {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, /* pipeMode= */ true);
ContentDataSource dataSource =
new ContentDataSource(ApplicationProvider.getApplicationContext());
DataSpec dataSpec = new DataSpec(contentUri, /* position= */ 1025, C.LENGTH_UNSET);
try {
ContentDataSourceException exception =
assertThrows(ContentDataSourceException.class, () -> dataSource.open(dataSpec));
assertThat(exception).hasCauseThat().isInstanceOf(EOFException.class);
} finally {
dataSource.close();
}
}
private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
ContentDataSource dataSource =
@ -130,5 +97,4 @@ public final class ContentDataSourceTest {
dataSource.close();
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2020 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.upstream;
import android.content.res.Resources;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.core.test.R;
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import org.junit.runner.RunWith;
/** {@link DataSource} contract tests for {@link RawResourceDataSource}. */
@RunWith(AndroidJUnit4.class)
public final class RawResourceDataSourceContractTest extends DataSourceContractTest {
private static final byte[] RESOURCE_1_DATA = Util.getUtf8Bytes("resource1 abc\n");
private static final byte[] RESOURCE_2_DATA = Util.getUtf8Bytes("resource2 abcdef\n");
@Override
protected DataSource createDataSource() {
return new RawResourceDataSource(ApplicationProvider.getApplicationContext());
}
@Override
protected ImmutableList<TestResource> getTestResources() {
// Android packages raw resources into a single file. When reading a resource other than the
// last one, Android does not prevent accidentally reading beyond the end of the resource and
// into the next one. We use two resources in this test to ensure that when packaged, at least
// one of them has a subsequent resource. This allows the contract test to enforce that the
// RawResourceDataSource implementation doesn't erroneously read into the second resource when
// opened to read the first.
return ImmutableList.of(
new TestResource.Builder()
.setName("resource 1")
.setUri(RawResourceDataSource.buildRawResourceUri(R.raw.resource1))
.setExpectedBytes(RESOURCE_1_DATA)
.build(),
new TestResource.Builder()
.setName("resource 2")
.setUri(RawResourceDataSource.buildRawResourceUri(R.raw.resource2))
.setExpectedBytes(RESOURCE_2_DATA)
.build(),
// Additional resources using different URI schemes.
new TestResource.Builder()
.setName("android.resource:// with path")
.setUri(
Uri.parse(
"android.resource://"
+ ApplicationProvider.getApplicationContext().getPackageName()
+ "/raw/resource1"))
.setExpectedBytes(RESOURCE_1_DATA)
.build(),
new TestResource.Builder()
.setName("android.resource:// with ID")
.setUri(
Uri.parse(
"android.resource://"
+ ApplicationProvider.getApplicationContext().getPackageName()
+ "/"
+ R.raw.resource1))
.setExpectedBytes(RESOURCE_1_DATA)
.build());
}
@Override
protected Uri getNotFoundUri() {
return RawResourceDataSource.buildRawResourceUri(Resources.ID_NULL);
}
}

View File

@ -24,7 +24,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@ -73,7 +72,7 @@ public final class TestContentProvider extends ContentProvider
openPipeHelper(
uri, /* mimeType= */ null, /* opts= */ null, /* args= */ null, /* func= */ this);
return new AssetFileDescriptor(
fileDescriptor, /* startOffset= */ 0, /* length= */ C.LENGTH_UNSET);
fileDescriptor, /* startOffset= */ 0, AssetFileDescriptor.UNKNOWN_LENGTH);
} else {
return getContext().getAssets().openFd(fileName);
}

View File

@ -0,0 +1 @@
resource1 abc

View File

@ -0,0 +1 @@
resource2 abcdef

View File

@ -669,6 +669,8 @@ public class DefaultRenderersFactory implements RenderersFactory {
new DefaultAudioProcessorChain(),
enableFloatOutput,
enableAudioTrackPlaybackParams,
enableOffload);
enableOffload
? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
: DefaultAudioSink.OFFLOAD_MODE_DISABLED);
}
}

View File

@ -74,7 +74,8 @@ import java.util.List;
* provides default implementations for common media types ({@link MediaCodecVideoRenderer},
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
* Renderer consumes media from the MediaSource being played. Renderers are injected when the
* player is created.
* player is created. The number of renderers and their respective track types can be obtained
* by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
* <li>A <b>{@link TrackSelector}</b> that selects tracks provided by the MediaSource to be
* consumed by each of the available Renderers. The library provides a default implementation
* ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected
@ -449,6 +450,20 @@ public interface ExoPlayer extends Player {
}
}
/** Returns the number of renderers. */
int getRendererCount();
/**
* Returns the track type that the renderer at a given index handles.
*
* <p>For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will
* return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}.
*
* @param index The index of the renderer.
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
*/
int getRendererType(int index);
/**
* Returns the track selector that this player uses, or null if track selection is not supported.
*/
@ -663,7 +678,7 @@ public interface ExoPlayer extends Player {
* <li>Audio offload rendering is enabled in {@link
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
* DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, boolean)}.
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, int)}.
* <li>An audio track is playing in a format that the device supports offloading (for example,
* MP3 or AAC).
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.
@ -682,6 +697,7 @@ public interface ExoPlayer extends Player {
* Returns whether the player has paused its main loop to save power in offload scheduling mode.
*
* @see #experimentalSetOffloadSchedulingEnabled(boolean)
* @see EventListener#onExperimentalSleepingForOffloadChanged(boolean)
*/
boolean experimentalIsSleepingForOffload();
}

View File

@ -999,7 +999,16 @@ import java.util.List;
if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) {
listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED,
listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason));
listener -> {
@Nullable Object manifest = null;
if (newPlaybackInfo.timeline.getWindowCount() == 1) {
// Legacy behavior was to report the manifest for single window timelines only.
Timeline.Window window = new Timeline.Window();
manifest = newPlaybackInfo.timeline.getWindow(0, window).manifest;
}
listener.onTimelineChanged(newPlaybackInfo.timeline, manifest, timelineChangeReason);
listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason);
});
}
if (positionDiscontinuity) {
listeners.queueEvent(
@ -1042,7 +1051,10 @@ import java.util.List;
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
listeners.queueEvent(
Player.EVENT_IS_LOADING_CHANGED,
listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading));
listener -> {
listener.onLoadingChanged(newPlaybackInfo.isLoading);
listener.onIsLoadingChanged(newPlaybackInfo.isLoading);
});
}
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState
|| previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {

View File

@ -1980,7 +1980,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod();
if (readingPeriod == null
|| queue.getPlayingPeriod() == readingPeriod
|| readingPeriod.allRenderersEnabled) {
|| readingPeriod.allRenderersInCorrectState) {
// Not reading ahead or all renderers updated.
return;
}
@ -2075,7 +2075,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();
return nextPlayingPeriodHolder != null
&& rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime()
&& nextPlayingPeriodHolder.allRenderersEnabled;
&& nextPlayingPeriodHolder.allRenderersInCorrectState;
}
private boolean hasReadingPeriodFinishedReading() {
@ -2294,7 +2294,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
enableRenderer(i, rendererWasEnabledFlags[i]);
}
}
readingMediaPeriod.allRenderersEnabled = true;
readingMediaPeriod.allRenderersInCorrectState = true;
}
private void enableRenderer(int rendererIndex, boolean wasRendererEnabled)

View File

@ -53,12 +53,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** {@link MediaPeriodInfo} about this media period. */
public MediaPeriodInfo info;
/**
* Whether all required renderers have been enabled with the {@link #sampleStreams} for this
* Whether all renderers are in the correct state for this {@link #mediaPeriod}.
*
* <p>Renderers that are needed must have been enabled with the {@link #sampleStreams} for this
* {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[],
* SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[],
* SampleStream, long)} has been called.
* SampleStream, long, boolean, boolean, long, long)} or {@link Renderer#replaceStream(Format[],
* SampleStream, long, long)} has been called.
*
* <p>Renderers that are not needed must have been {@link Renderer#disable() disabled}.
*/
public boolean allRenderersEnabled;
public boolean allRenderersInCorrectState;
private final boolean[] mayRetainStreamFlags;
private final RendererCapabilities[] rendererCapabilities;

View File

@ -21,6 +21,7 @@ import static java.lang.Math.min;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MaskingMediaPeriod;
@ -600,9 +601,11 @@ import java.util.Set;
@Override
public void onDrmSessionAcquired(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
@DrmSession.State int state) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionAcquired();
drmEventDispatcher.drmSessionAcquired(state);
}
}

View File

@ -240,7 +240,7 @@ public interface Renderer extends PlayerMessage.Target {
/**
* Returns the track type that the renderer handles.
*
* @see Player#getRendererType(int)
* @see ExoPlayer#getRendererType(int)
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
*/
int getTrackType();

View File

@ -1293,13 +1293,6 @@ public class SimpleExoPlayer extends BasePlayer
prepare();
}
@Override
public void setMediaItems(List<MediaItem> mediaItems) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItems(mediaItems);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
verifyApplicationThread();
@ -1315,27 +1308,6 @@ public class SimpleExoPlayer extends BasePlayer
player.setMediaItems(mediaItems, startWindowIndex, startPositionMs);
}
@Override
public void setMediaItem(MediaItem mediaItem) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItem(mediaItem);
}
@Override
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItem(mediaItem, resetPosition);
}
@Override
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItem(mediaItem, startPositionMs);
}
@Override
public void setMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread();
@ -1391,18 +1363,6 @@ public class SimpleExoPlayer extends BasePlayer
player.addMediaItems(index, mediaItems);
}
@Override
public void addMediaItem(MediaItem mediaItem) {
verifyApplicationThread();
player.addMediaItem(mediaItem);
}
@Override
public void addMediaItem(int index, MediaItem mediaItem) {
verifyApplicationThread();
player.addMediaItem(index, mediaItem);
}
@Override
public void addMediaSource(MediaSource mediaSource) {
verifyApplicationThread();
@ -1427,24 +1387,12 @@ public class SimpleExoPlayer extends BasePlayer
player.addMediaSources(index, mediaSources);
}
@Override
public void moveMediaItem(int currentIndex, int newIndex) {
verifyApplicationThread();
player.moveMediaItem(currentIndex, newIndex);
}
@Override
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
verifyApplicationThread();
player.moveMediaItems(fromIndex, toIndex, newIndex);
}
@Override
public void removeMediaItem(int index) {
verifyApplicationThread();
player.removeMediaItem(index);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThread();
@ -2072,8 +2020,8 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public void onRenderedFirstFrame(Surface surface) {
analyticsCollector.onRenderedFirstFrame(surface);
public void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {
analyticsCollector.onRenderedFirstFrame(surface, renderTimeMs);
if (SimpleExoPlayer.this.surface == surface) {
for (VideoListener videoListener : videoListeners) {
videoListener.onRenderedFirstFrame();

View File

@ -37,6 +37,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo;
@ -207,7 +208,7 @@ public class AnalyticsCollector
// AudioRendererEventListener implementation.
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioEnabled(DecoderCounters counters) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -220,7 +221,7 @@ public class AnalyticsCollector
});
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
@ -230,12 +231,14 @@ public class AnalyticsCollector
AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED,
listener -> {
listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs);
listener.onAudioDecoderInitialized(
eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
listener.onDecoderInitialized(
eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
});
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
@ -244,6 +247,7 @@ public class AnalyticsCollector
eventTime,
AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED,
listener -> {
listener.onAudioInputFormatChanged(eventTime, format);
listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation);
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
});
@ -278,7 +282,7 @@ public class AnalyticsCollector
listener -> listener.onAudioDecoderReleased(eventTime, decoderName));
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioDisabled(DecoderCounters counters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
@ -361,7 +365,7 @@ public class AnalyticsCollector
// VideoRendererEventListener implementation.
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoEnabled(DecoderCounters counters) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -374,7 +378,7 @@ public class AnalyticsCollector
});
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
@ -384,12 +388,14 @@ public class AnalyticsCollector
AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED,
listener -> {
listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs);
listener.onVideoDecoderInitialized(
eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
listener.onDecoderInitialized(
eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
});
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
@ -398,6 +404,7 @@ public class AnalyticsCollector
eventTime,
AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED,
listener -> {
listener.onVideoInputFormatChanged(eventTime, format);
listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation);
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
});
@ -421,7 +428,7 @@ public class AnalyticsCollector
listener -> listener.onVideoDecoderReleased(eventTime, decoderName));
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoDisabled(DecoderCounters counters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
@ -446,13 +453,17 @@ public class AnalyticsCollector
eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio));
}
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onRenderedFirstFrame(@Nullable Surface surface) {
public final void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_RENDERED_FIRST_FRAME,
listener -> listener.onRenderedFirstFrame(eventTime, surface));
listener -> {
listener.onRenderedFirstFrame(eventTime, surface);
listener.onRenderedFirstFrame(eventTime, surface, renderTimeMs);
});
}
@Override
@ -615,16 +626,20 @@ public class AnalyticsCollector
listener -> listener.onStaticMetadataChanged(eventTime, metadataList));
}
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onIsLoadingChanged(boolean isLoading) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_IS_LOADING_CHANGED,
listener -> listener.onIsLoadingChanged(eventTime, isLoading));
listener -> {
listener.onLoadingChanged(eventTime, isLoading);
listener.onIsLoadingChanged(eventTime, isLoading);
});
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
@Override
public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
@ -725,7 +740,7 @@ public class AnalyticsCollector
listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters));
}
@SuppressWarnings("deprecation")
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
@Override
public final void onSeekProcessed() {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
@ -747,12 +762,17 @@ public class AnalyticsCollector
// DefaultDrmSessionManager.EventListener implementation.
@Override
public final void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
@SuppressWarnings("deprecation") // Calls deprecated listener method.
public final void onDrmSessionAcquired(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
sendEvent(
eventTime,
AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED,
listener -> listener.onDrmSessionAcquired(eventTime));
listener -> {
listener.onDrmSessionAcquired(eventTime);
listener.onDrmSessionAcquired(eventTime, state);
});
}
@Override

View File

@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.os.Looper;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.Surface;
import androidx.annotation.IntDef;
@ -39,6 +40,7 @@ import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderException;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
@ -583,10 +585,7 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param isLoading Whether the player is loading.
*/
@SuppressWarnings("deprecation")
default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {
onLoadingChanged(eventTime, isLoading);
}
default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {}
/** @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead. */
@Deprecated
@ -755,8 +754,18 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
* @param decoderName The decoder that was created.
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
default void onAudioDecoderInitialized(
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {}
/** @deprecated Use {@link #onAudioDecoderInitialized(EventTime, String, long, long)}. */
@Deprecated
default void onAudioDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {}
@ -775,11 +784,10 @@ public interface AnalyticsListener {
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@SuppressWarnings("deprecation")
default void onAudioInputFormatChanged(
EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
onAudioInputFormatChanged(eventTime, format);
}
EventTime eventTime,
Format format,
@Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
/**
* Called when the audio position has increased for the first time since the last pause or
@ -898,8 +906,18 @@ public interface AnalyticsListener {
*
* @param eventTime The event time.
* @param decoderName The decoder that was created.
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
default void onVideoDecoderInitialized(
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {}
/** @deprecated Use {@link #onVideoDecoderInitialized(EventTime, String, long, long)}. */
@Deprecated
default void onVideoDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {}
@ -918,11 +936,10 @@ public interface AnalyticsListener {
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@SuppressWarnings("deprecation")
default void onVideoInputFormatChanged(
EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
onVideoInputFormatChanged(eventTime, format);
}
EventTime eventTime,
Format format,
@Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
/**
* Called after video frames have been dropped.
@ -992,7 +1009,13 @@ public interface AnalyticsListener {
* @param eventTime The event time.
* @param surface The {@link Surface} to which a frame has been rendered, or {@code null} if the
* renderer renders to something that isn't a {@link Surface}.
* @param renderTimeMs {@link SystemClock#elapsedRealtime()} when the first frame was rendered.
*/
default void onRenderedFirstFrame(
EventTime eventTime, @Nullable Surface surface, long renderTimeMs) {}
/** @deprecated Use {@link #onRenderedFirstFrame(EventTime, Surface, long)} instead. */
@Deprecated
default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {}
/**
@ -1026,12 +1049,17 @@ public interface AnalyticsListener {
*/
default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}
/** @deprecated Implement {@link #onDrmSessionAcquired(EventTime, int)} instead. */
@Deprecated
default void onDrmSessionAcquired(EventTime eventTime) {}
/**
* Called each time a drm session is acquired.
*
* @param eventTime The event time.
* @param state The {@link DrmSession.State} of the session when the acquisition completed.
*/
default void onDrmSessionAcquired(EventTime eventTime) {}
default void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {}
/**
* Called each time drm keys are loaded.

View File

@ -69,11 +69,8 @@ public interface AudioRendererEventListener {
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@SuppressWarnings("deprecation")
default void onAudioInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
onAudioInputFormatChanged(format);
}
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
/**
* Called when the audio position has increased for the first time since the last pause or
@ -186,11 +183,15 @@ public interface AudioRendererEventListener {
}
/** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */
@SuppressWarnings("deprecation") // Calling deprecated listener method.
public void inputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
if (handler != null) {
handler.post(
() -> castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation));
() -> {
castNonNull(listener).onAudioInputFormatChanged(format);
castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation);
});
}
}

View File

@ -428,7 +428,7 @@ public interface AudioSink {
/**
* Sets the playback volume.
*
* @param volume A volume in the range [0.0, 1.0].
* @param volume Linear output gain to apply to all channels. Should be in the range [0.0, 1.0].
*/
void setVolume(float volume);

View File

@ -215,6 +215,35 @@ public final class DefaultAudioSink implements AudioSink {
/** The default skip silence flag. */
private static final boolean DEFAULT_SKIP_SILENCE = false;
/** Audio offload mode configuration. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
OFFLOAD_MODE_DISABLED,
OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED,
OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED
})
public @interface OffloadMode {}
/** The audio sink will never play in offload mode. */
public static final int OFFLOAD_MODE_DISABLED = 0;
/**
* The audio sink will prefer offload playback except if the track is gapless and the device does
* not advertise support for gapless playback in offload.
*
* <p>Use this option to prioritize seamless transitions between tracks of the same album to power
* savings.
*/
public static final int OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED = 1;
/**
* The audio sink will prefer offload playback even if this might result in silence gaps between
* tracks.
*
* <p>Use this option to prioritize battery saving at the cost of a possible non seamless
* transitions between tracks of the same album.
*/
public static final int OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED = 2;
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH})
@ -281,7 +310,7 @@ public final class DefaultAudioSink implements AudioSink {
private final AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
private final boolean enableAudioTrackPlaybackParams;
private final boolean enableOffload;
@OffloadMode private final int offloadMode;
@MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29;
private final PendingExceptionHolder<InitializationException>
initializationExceptionPendingExceptionHolder;
@ -364,7 +393,7 @@ public final class DefaultAudioSink implements AudioSink {
new DefaultAudioProcessorChain(audioProcessors),
enableFloatOutput,
/* enableAudioTrackPlaybackParams= */ false,
/* enableOffload= */ false);
OFFLOAD_MODE_DISABLED);
}
/**
@ -382,8 +411,8 @@ public final class DefaultAudioSink implements AudioSink {
* use.
* @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
* android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
* @param enableOffload Whether to enable audio offload. If an audio format can be both played
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is
* @param offloadMode Audio offload configuration. If an audio format can be both played with
* offload and encoded audio passthrough, it will be played in offload. Audio offload is
* supported from API level 29. Most Android devices can only support one offload {@link
* android.media.AudioTrack} at a time and can invalidate it at any time. Thus an app can
* never be guaranteed that it will be able to play in offload. Audio processing (for example,
@ -394,12 +423,12 @@ public final class DefaultAudioSink implements AudioSink {
AudioProcessorChain audioProcessorChain,
boolean enableFloatOutput,
boolean enableAudioTrackPlaybackParams,
boolean enableOffload) {
@OffloadMode int offloadMode) {
this.audioCapabilities = audioCapabilities;
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput;
this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams;
this.enableOffload = Util.SDK_INT >= 29 && enableOffload;
this.offloadMode = Util.SDK_INT >= 29 ? offloadMode : OFFLOAD_MODE_DISABLED;
releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
@ -462,9 +491,7 @@ public final class DefaultAudioSink implements AudioSink {
// guaranteed to support.
return SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
}
if (enableOffload
&& !offloadDisabledUntilNextConfiguration
&& isOffloadedPlaybackSupported(format, audioAttributes)) {
if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) {
return SINK_FORMAT_SUPPORTED_DIRECTLY;
}
if (isPassthroughPlaybackSupported(format, audioCapabilities)) {
@ -541,7 +568,7 @@ public final class DefaultAudioSink implements AudioSink {
availableAudioProcessors = new AudioProcessor[0];
outputSampleRate = inputFormat.sampleRate;
outputPcmFrameSize = C.LENGTH_UNSET;
if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) {
if (useOffloadedPlayback(inputFormat, audioAttributes)) {
outputMode = OUTPUT_MODE_OFFLOAD;
outputEncoding =
MimeTypes.getEncoding(
@ -1478,6 +1505,10 @@ public final class DefaultAudioSink implements AudioSink {
&& !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) {
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
encoding = C.ENCODING_E_AC3;
} else if (encoding == C.ENCODING_DTS_HD
&& !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) {
// DTS receivers support DTS-HD streams (but decode only the core layer).
encoding = C.ENCODING_DTS;
}
if (!audioCapabilities.supportsEncoding(encoding)) {
return null;
@ -1561,9 +1592,8 @@ public final class DefaultAudioSink implements AudioSink {
return Util.getAudioTrackChannelConfig(channelCount);
}
private static boolean isOffloadedPlaybackSupported(
Format format, AudioAttributes audioAttributes) {
if (Util.SDK_INT < 29) {
private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) {
if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) {
return false;
}
@C.Encoding
@ -1581,8 +1611,12 @@ public final class DefaultAudioSink implements AudioSink {
audioFormat, audioAttributes.getAudioAttributesV21())) {
return false;
}
boolean notGapless = format.encoderDelay == 0 && format.encoderPadding == 0;
return notGapless || isOffloadedGaplessPlaybackSupported();
boolean isGapless = format.encoderDelay != 0 || format.encoderPadding != 0;
boolean offloadRequiresGaplessSupport = offloadMode == OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED;
if (isGapless && offloadRequiresGaplessSupport && !isOffloadedGaplessPlaybackSupported()) {
return false;
}
return true;
}
private static boolean isOffloadedPlayback(AudioTrack audioTrack) {

View File

@ -293,11 +293,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
if (openInternal(true)) {
doLicense(true);
}
} else if (eventDispatcher != null && isOpen()) {
// If the session is already open then send the acquire event only to the provided dispatcher.
// TODO: Add a parameter to onDrmSessionAcquired to indicate whether the session is being
// re-used or not.
eventDispatcher.drmSessionAcquired();
} else if (eventDispatcher != null
&& isOpen()
&& eventDispatchers.count(eventDispatcher) == 1) {
// If the session is already open and this is the first instance of eventDispatcher we've
// seen, then send the acquire event only to the provided dispatcher.
eventDispatcher.drmSessionAcquired(state);
}
referenceCountListener.onReferenceCountIncremented(this, referenceCount);
}
@ -321,15 +322,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
mediaDrm.closeSession(sessionId);
sessionId = null;
}
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmSessionReleased);
}
if (eventDispatcher != null) {
if (isOpen()) {
// If the session is still open then send the release event only to the provided dispatcher
// before removing it.
eventDispatchers.remove(eventDispatcher);
if (eventDispatchers.count(eventDispatcher) == 0) {
// Release events are only sent to the last-attached instance of each EventDispatcher.
eventDispatcher.drmSessionReleased();
}
eventDispatchers.remove(eventDispatcher);
}
referenceCountListener.onReferenceCountDecremented(this, referenceCount);
}
@ -353,8 +352,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
try {
sessionId = mediaDrm.openSession();
mediaCrypto = mediaDrm.createMediaCrypto(sessionId);
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmSessionAcquired);
state = STATE_OPENED;
// Capture state into a local so a consistent value is seen by the lambda.
int localState = state;
dispatchEvent(eventDispatcher -> eventDispatcher.drmSessionAcquired(localState));
Assertions.checkNotNull(sessionId);
return true;
} catch (NotProvisionedException e) {

View File

@ -15,6 +15,10 @@
*/
package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.annotation.SuppressLint;
import android.media.ResourceBusyException;
import android.os.Handler;
@ -31,7 +35,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@ -47,9 +50,16 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */
/**
* A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}.
*
* <p>This implementation supports pre-acquisition of sessions using {@link
* #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
*/
@RequiresApi(18)
public class DefaultDrmSessionManager implements DrmSessionManager {
@ -120,8 +130,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
*/
public Builder setUuidAndExoMediaDrmProvider(
UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {
this.uuid = Assertions.checkNotNull(uuid);
this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider);
this.uuid = checkNotNull(uuid);
this.exoMediaDrmProvider = checkNotNull(exoMediaDrmProvider);
return this;
}
@ -157,8 +167,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
public Builder setUseDrmSessionsForClearContent(
int... useDrmSessionsForClearContentTrackTypes) {
for (int trackType : useDrmSessionsForClearContentTrackTypes) {
Assertions.checkArgument(
trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
checkArgument(trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
}
this.useDrmSessionsForClearContentTrackTypes =
useDrmSessionsForClearContentTrackTypes.clone();
@ -185,7 +194,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* @return This builder.
*/
public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy);
this.loadErrorHandlingPolicy = checkNotNull(loadErrorHandlingPolicy);
return this;
}
@ -205,7 +214,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* @return This builder.
*/
public Builder setSessionKeepaliveMs(long sessionKeepaliveMs) {
Assertions.checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET);
checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET);
this.sessionKeepaliveMs = sessionKeepaliveMs;
return this;
}
@ -282,14 +291,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private final List<DefaultDrmSession> sessions;
private final List<DefaultDrmSession> provisioningSessions;
private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
private final Set<DefaultDrmSession> keepaliveSessions;
private int prepareCallsCount;
@Nullable private ExoMediaDrm exoMediaDrm;
@Nullable private DefaultDrmSession placeholderDrmSession;
@Nullable private DefaultDrmSession noMultiSessionDrmSession;
@Nullable private Looper playbackLooper;
private @MonotonicNonNull Handler sessionReleasingHandler;
private @MonotonicNonNull Looper playbackLooper;
private @MonotonicNonNull Handler playbackHandler;
private int mode;
@Nullable private byte[] offlineLicenseKeySetId;
@ -388,8 +398,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
boolean playClearSamplesWithoutKeys,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
long sessionKeepaliveMs) {
Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
checkNotNull(uuid);
checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid;
this.exoMediaDrmProvider = exoMediaDrmProvider;
this.callback = callback;
@ -403,6 +413,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
mode = MODE_PLAYBACK;
sessions = new ArrayList<>();
provisioningSessions = new ArrayList<>();
preacquiredSessionReferences = Sets.newIdentityHashSet();
keepaliveSessions = Sets.newIdentityHashSet();
this.sessionKeepaliveMs = sessionKeepaliveMs;
}
@ -432,9 +443,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
*/
public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {
Assertions.checkState(sessions.isEmpty());
checkState(sessions.isEmpty());
if (mode == MODE_QUERY || mode == MODE_RELEASE) {
Assertions.checkNotNull(offlineLicenseKeySetId);
checkNotNull(offlineLicenseKeySetId);
}
this.mode = mode;
this.offlineLicenseKeySetId = offlineLicenseKeySetId;
@ -447,7 +458,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (prepareCallsCount++ != 0) {
return;
}
Assertions.checkState(exoMediaDrm == null);
checkState(exoMediaDrm == null);
exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
}
@ -466,10 +477,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
sessions.get(i).release(/* eventDispatcher= */ null);
}
}
Assertions.checkNotNull(exoMediaDrm).release();
releaseAllPreacquiredSessions();
checkNotNull(exoMediaDrm).release();
exoMediaDrm = null;
}
@Override
public DrmSessionReference preacquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
initPlaybackLooper(playbackLooper);
PreacquiredSessionReference preacquiredSessionReference =
new PreacquiredSessionReference(eventDispatcher);
preacquiredSessionReference.acquire(format);
return preacquiredSessionReference;
}
@Override
@Nullable
public DrmSession acquireSession(
@ -477,16 +502,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
initPlaybackLooper(playbackLooper);
return acquireSession(
playbackLooper,
eventDispatcher,
format,
/* shouldReleasePreacquiredSessionsBeforeRetrying= */ true);
}
// Must be called on the playback thread.
@Nullable
private DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format,
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
maybeCreateMediaDrmHandler(playbackLooper);
if (format.drmInitData == null) {
// Content is not encrypted.
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType));
return maybeAcquirePlaceholderSession(
MimeTypes.getTrackType(format.sampleMimeType),
shouldReleasePreacquiredSessionsBeforeRetrying);
}
@Nullable List<SchemeData> schemeDatas = null;
if (offlineLicenseKeySetId == null) {
schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false);
schemeDatas = getSchemeDatas(checkNotNull(format.drmInitData), uuid, false);
if (schemeDatas.isEmpty()) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
Log.e(TAG, "DRM error", error);
@ -515,7 +556,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Create a new session.
session =
createAndAcquireSessionWithRetry(
schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher);
schemeDatas,
/* isPlaceholderSession= */ false,
eventDispatcher,
shouldReleasePreacquiredSessionsBeforeRetrying);
if (!multiSession) {
noMultiSessionDrmSession = session;
}
@ -531,7 +575,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable
public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format) {
Class<? extends ExoMediaCrypto> exoMediaCryptoType =
Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType();
checkNotNull(exoMediaDrm).getExoMediaCryptoType();
if (format.drmInitData == null) {
int trackType = MimeTypes.getTrackType(format.sampleMimeType);
return Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) != C.INDEX_UNSET
@ -547,8 +591,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Internal methods.
@Nullable
private DrmSession maybeAcquirePlaceholderSession(int trackType) {
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
private DrmSession maybeAcquirePlaceholderSession(
int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
ExoMediaDrm exoMediaDrm = checkNotNull(this.exoMediaDrm);
boolean avoidPlaceholderDrmSessions =
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
@ -563,7 +608,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
createAndAcquireSessionWithRetry(
/* schemeDatas= */ ImmutableList.of(),
/* isPlaceholderSession= */ true,
/* eventDispatcher= */ null);
/* eventDispatcher= */ null,
shouldReleasePreacquiredSessionsBeforeRetrying);
sessions.add(placeholderDrmSession);
this.placeholderDrmSession = placeholderDrmSession;
} else {
@ -607,12 +653,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
return true;
}
private void initPlaybackLooper(Looper playbackLooper) {
@EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"})
private synchronized void initPlaybackLooper(Looper playbackLooper) {
if (this.playbackLooper == null) {
this.playbackLooper = playbackLooper;
this.sessionReleasingHandler = new Handler(playbackLooper);
this.playbackHandler = new Handler(playbackLooper);
} else {
Assertions.checkState(this.playbackLooper == playbackLooper);
checkState(this.playbackLooper == playbackLooper);
checkNotNull(playbackHandler);
}
}
@ -625,35 +673,67 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private DefaultDrmSession createAndAcquireSessionWithRetry(
@Nullable List<SchemeData> schemeDatas,
boolean isPlaceholderSession,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
DefaultDrmSession session =
createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
if (session.getState() == DrmSession.STATE_ERROR
&& (Util.SDK_INT < 19
|| Assertions.checkNotNull(session.getError()).getCause()
instanceof ResourceBusyException)) {
// We're short on DRM session resources, so eagerly release all our keepalive sessions.
// ResourceBusyException is only available at API 19, so on earlier versions we always
// eagerly release regardless of the underlying error.
if (!keepaliveSessions.isEmpty()) {
// Make a local copy, because sessions are removed from this.keepaliveSessions during
// release (via callback).
ImmutableSet<DefaultDrmSession> keepaliveSessions =
ImmutableSet.copyOf(this.keepaliveSessions);
for (DrmSession keepaliveSession : keepaliveSessions) {
keepaliveSession.release(/* eventDispatcher= */ null);
}
// Undo the acquisitions from createAndAcquireSession().
session.release(eventDispatcher);
if (sessionKeepaliveMs != C.TIME_UNSET) {
session.release(/* eventDispatcher= */ null);
}
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
// If we're short on DRM session resources, first try eagerly releasing all our keepalive
// sessions and then retry the acquisition.
if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) {
// Make a local copy, because sessions are removed from this.keepaliveSessions during
// release (via callback).
ImmutableSet<DefaultDrmSession> keepaliveSessions =
ImmutableSet.copyOf(this.keepaliveSessions);
for (DrmSession keepaliveSession : keepaliveSessions) {
keepaliveSession.release(/* eventDispatcher= */ null);
}
undoAcquisition(session, eventDispatcher);
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
}
// If the acquisition failed again due to continued resource shortage, and
// shouldReleasePreacquiredSessionsBeforeRetrying is true, try releasing all pre-acquired
// sessions and then retry the acquisition.
if (acquisitionFailedIndicatingResourceShortage(session)
&& shouldReleasePreacquiredSessionsBeforeRetrying
&& !preacquiredSessionReferences.isEmpty()) {
releaseAllPreacquiredSessions();
undoAcquisition(session, eventDispatcher);
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
}
return session;
}
private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) {
// ResourceBusyException is only available at API 19, so on earlier versions we
// assume any error indicates resource shortage (ensuring we retry).
return session.getState() == DrmSession.STATE_ERROR
&& (Util.SDK_INT < 19
|| checkNotNull(session.getError()).getCause() instanceof ResourceBusyException);
}
/**
* Undoes the acquisitions from {@link #createAndAcquireSession(List, boolean,
* DrmSessionEventListener.EventDispatcher)}.
*/
private void undoAcquisition(
DrmSession session, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
session.release(eventDispatcher);
if (sessionKeepaliveMs != C.TIME_UNSET) {
session.release(/* eventDispatcher= */ null);
}
}
private void releaseAllPreacquiredSessions() {
// Make a local copy, because sessions are removed from this.preacquiredSessionReferences
// during release (via callback).
ImmutableSet<PreacquiredSessionReference> preacquiredSessionReferences =
ImmutableSet.copyOf(this.preacquiredSessionReferences);
for (PreacquiredSessionReference preacquiredSessionReference : preacquiredSessionReferences) {
preacquiredSessionReference.release();
}
}
/**
* Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in
* {@code eventDispatcher}).
@ -665,7 +745,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable List<SchemeData> schemeDatas,
boolean isPlaceholderSession,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
Assertions.checkNotNull(exoMediaDrm);
checkNotNull(exoMediaDrm);
// Placeholder sessions should always play clear samples without keys.
boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;
DefaultDrmSession session =
@ -681,7 +761,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
offlineLicenseKeySetId,
keyRequestParameters,
callback,
Assertions.checkNotNull(playbackLooper),
checkNotNull(playbackLooper),
loadErrorHandlingPolicy);
// Acquire the session once on behalf of the caller to DrmSessionManager - this is the
// reference 'assigned' to the caller which they're responsible for releasing. Do this first,
@ -782,7 +862,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (sessionKeepaliveMs != C.TIME_UNSET) {
// The session has been acquired elsewhere so we want to cancel our timeout.
keepaliveSessions.remove(session);
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
}
}
@ -791,7 +871,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) {
// Only the internal keep-alive reference remains, so we can start the timeout.
keepaliveSessions.add(session);
Assertions.checkNotNull(sessionReleasingHandler)
checkNotNull(playbackHandler)
.postAtTime(
() -> session.release(/* eventDispatcher= */ null),
session,
@ -812,7 +892,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
}
provisioningSessions.remove(session);
if (sessionKeepaliveMs != C.TIME_UNSET) {
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
keepaliveSessions.remove(session);
}
}
@ -824,7 +904,78 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Override
public void onEvent(
ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) {
Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
}
}
/**
* An implementation of {@link DrmSessionReference} that lazily acquires the underlying {@link
* DrmSession}.
*
* <p>A new instance is needed for each reference (compared to maintaining exactly one instance
* for each {@link DrmSession}) because each associated {@link
* DrmSessionEventListener.EventDispatcher} might be different. The {@link
* DrmSessionEventListener.EventDispatcher} is required to implement the zero-arg {@link
* DrmSessionReference#release()} method.
*/
private class PreacquiredSessionReference implements DrmSessionReference {
@Nullable private final DrmSessionEventListener.EventDispatcher eventDispatcher;
@Nullable private DrmSession session;
private boolean isReleased;
/**
* Constructs an instance.
*
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link
* #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
*/
public PreacquiredSessionReference(
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
this.eventDispatcher = eventDispatcher;
}
/**
* Acquires the underlying session.
*
* <p>Must be called at most once. Can be called from any thread.
*/
@RequiresNonNull("playbackHandler")
public void acquire(Format format) {
playbackHandler.post(
() -> {
if (prepareCallsCount == 0 || isReleased) {
// The manager has been fully released or this reference has already been released.
// Abort the acquisition attempt.
return;
}
this.session =
acquireSession(
checkNotNull(playbackLooper),
eventDispatcher,
format,
/* shouldReleasePreacquiredSessionsBeforeRetrying= */ false);
preacquiredSessionReferences.add(this);
});
}
@Override
public void release() {
// Ensure the underlying session is released immediately if we're already on the playback
// thread, to allow a failed session opening to be immediately retried.
Util.postOrRun(
checkNotNull(playbackHandler),
() -> {
if (isReleased) {
return;
}
if (session != null) {
session.release(eventDispatcher);
}
preacquiredSessionReferences.remove(this);
isReleased = true;
});
}
}
}

View File

@ -28,13 +28,19 @@ import java.util.concurrent.CopyOnWriteArrayList;
/** Listener of {@link DrmSessionManager} events. */
public interface DrmSessionEventListener {
/** @deprecated Implement {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} instead. */
@Deprecated
default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}
/**
* Called each time a drm session is acquired.
*
* @param windowIndex The window index in the timeline this media period belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
* @param state The {@link DrmSession.State} of the session when the acquisition completed.
*/
default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}
default void onDrmSessionAcquired(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {}
/**
* Called each time keys are loaded.
@ -149,13 +155,20 @@ public interface DrmSessionEventListener {
}
}
/** Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId)}. */
public void drmSessionAcquired() {
/**
* Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} and {@link
* #onDrmSessionAcquired(int, MediaPeriodId)}.
*/
@SuppressWarnings("deprecation") // Calls deprecated listener method.
public void drmSessionAcquired(@DrmSession.State int state) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
DrmSessionEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() -> listener.onDrmSessionAcquired(windowIndex, mediaPeriodId));
() -> {
listener.onDrmSessionAcquired(windowIndex, mediaPeriodId);
listener.onDrmSessionAcquired(windowIndex, mediaPeriodId, state);
});
}
}

View File

@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format;
/** Manages a DRM session. */
public interface DrmSessionManager {
/**
* Represents a single reference count of a {@link DrmSession}, while deliberately not giving
* access to the underlying session.
*/
interface DrmSessionReference {
/** A reference that is never populated with an underlying {@link DrmSession}. */
DrmSessionReference EMPTY = () -> {};
/**
* Releases the underlying session at most once.
*
* <p>Can be called from any thread. Calling this method more than once will only release the
* underlying session once.
*/
void release();
}
/** An instance that supports no DRM schemes. */
DrmSessionManager DRM_UNSUPPORTED =
new DrmSessionManager() {
@ -81,6 +98,51 @@ public interface DrmSessionManager {
// Do nothing.
}
/**
* Pre-acquires a DRM session for the specified {@link Format}.
*
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(Looper,
* DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely,
* allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready
* in the background.
*
* <p>The caller must call {@link DrmSessionReference#release()} on the returned instance when
* they no longer require the pre-acquisition (i.e. they know they won't be making a matching call
* to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near
* future).
*
* <p>This manager may silently release the underlying session in order to allow another operation
* to complete. This will result in a subsequent call to {@link #acquireSession(Looper,
* DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including
* repeating key loads and other async initialization steps.
*
* <p>The caller must separately call {@link #acquireSession(Looper,
* DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for
* playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances
* are distinct. The caller must release both, and can release the {@link DrmSessionReference}
* before the {@link DrmSession} without affecting playback.
*
* <p>This can be called from any thread.
*
* <p>Implementations that do not support pre-acquisition always return an empty {@link
* DrmSessionReference} instance.
*
* @param playbackLooper The looper associated with the media playback thread.
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
* events, and passed on to {@link
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
* @param format The {@link Format} for which to pre-acquire a {@link DrmSession}.
* @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching
* {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would
* return null.
*/
default DrmSessionReference preacquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
return DrmSessionReference.EMPTY;
}
/**
* Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is

View File

@ -733,11 +733,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throws ExoPlaybackException {
this.currentPlaybackSpeed = currentPlaybackSpeed;
this.targetPlaybackSpeed = targetPlaybackSpeed;
if (codec != null
&& codecDrainAction != DRAIN_ACTION_REINITIALIZE
&& getState() != STATE_DISABLED) {
updateCodecOperatingRate(codecInputFormat);
}
updateCodecOperatingRate(codecInputFormat);
}
@Override
@ -1689,6 +1685,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return CODEC_OPERATING_RATE_UNSET;
}
/**
* Updates the codec operating rate, or triggers codec release and re-initialization if a
* previously set operating rate needs to be cleared.
*
* @throws ExoPlaybackException If an error occurs releasing or initializing a codec.
* @return False if codec release and re-initialization was triggered. True in all other cases.
*/
protected final boolean updateCodecOperatingRate() throws ExoPlaybackException {
return updateCodecOperatingRate(codecInputFormat);
}
/**
* Updates the codec operating rate, or triggers codec release and re-initialization if a
* previously set operating rate needs to be cleared.
@ -1702,6 +1709,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return true;
}
if (codec == null
|| codecDrainAction == DRAIN_ACTION_REINITIALIZE
|| getState() == STATE_DISABLED) {
// No need to update the operating rate.
return true;
}
float newCodecOperatingRate =
getCodecOperatingRateV23(targetPlaybackSpeed, format, getStreamFormats());
if (codecOperatingRate == newCodecOperatingRate) {

View File

@ -19,6 +19,7 @@ import android.os.Handler;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@ -290,9 +291,10 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
// DrmSessionEventListener implementation
@Override
public void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
public void onDrmSessionAcquired(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionAcquired();
drmEventDispatcher.drmSessionAcquired(state);
}
}

View File

@ -336,7 +336,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource<Void> {
.setTag(tag)
.build(),
dataSourceFactory,
extractorsFactory,
() -> new BundledExtractorsAdapter(extractorsFactory),
DrmSessionManager.DRM_UNSUPPORTED,
loadableLoadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes);

View File

@ -26,7 +26,14 @@ import java.util.List;
import java.util.Map;
/** Extracts the contents of a container file from a progressive media stream. */
/* package */ interface ProgressiveMediaExtractor {
public interface ProgressiveMediaExtractor {
/** Creates {@link ProgressiveMediaExtractor} instances. */
interface Factory {
/** Returns a new {@link ProgressiveMediaExtractor} instance. */
ProgressiveMediaExtractor createProgressiveMediaExtractor();
}
/**
* Initializes the underlying infrastructure for reading from the input.

View File

@ -31,7 +31,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
@ -147,7 +146,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource The data source to read the media.
* @param extractorsFactory The {@link ExtractorsFactory} to use to read the data source.
* @param progressiveMediaExtractor The {@link ProgressiveMediaExtractor} to use to read the data
* source.
* @param drmSessionManager A {@link DrmSessionManager} to allow DRM interactions.
* @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events.
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
@ -168,7 +168,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public ProgressiveMediaPeriod(
Uri uri,
DataSource dataSource,
ExtractorsFactory extractorsFactory,
ProgressiveMediaExtractor progressiveMediaExtractor,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@ -188,7 +188,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("ProgressiveMediaPeriod");
this.progressiveMediaExtractor = new BundledExtractorsAdapter(extractorsFactory);
this.progressiveMediaExtractor = progressiveMediaExtractor;
loadCondition = new ConditionVariable();
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
onContinueLoadingRequestedRunnable =

View File

@ -54,7 +54,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private final DataSource.Factory dataSourceFactory;
private ExtractorsFactory extractorsFactory;
private ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
private boolean usingCustomDrmSessionManagerProvider;
private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@ -72,15 +72,26 @@ public final class ProgressiveMediaSource extends BaseMediaSource
this(dataSourceFactory, new DefaultExtractorsFactory());
}
/**
* Equivalent to {@link #Factory(DataSource.Factory, ProgressiveMediaExtractor.Factory) new
* Factory(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory)}.
*/
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory));
}
/**
* Creates a new factory for {@link ProgressiveMediaSource}s.
*
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
* @param extractorsFactory A factory for extractors used to extract media from its container.
* @param progressiveMediaExtractorFactory A factory for the {@link ProgressiveMediaExtractor}
* to extract media from its container.
*/
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
public Factory(
DataSource.Factory dataSourceFactory,
ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory) {
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
@ -93,8 +104,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
*/
@Deprecated
public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) {
this.extractorsFactory =
extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory();
this.progressiveMediaExtractorFactory =
() ->
new BundledExtractorsAdapter(
extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory());
return this;
}
@ -220,7 +233,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return new ProgressiveMediaSource(
mediaItem,
dataSourceFactory,
extractorsFactory,
progressiveMediaExtractorFactory,
drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes);
@ -241,7 +254,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private final MediaItem mediaItem;
private final MediaItem.PlaybackProperties playbackProperties;
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
private final int continueLoadingCheckIntervalBytes;
@ -256,14 +269,14 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/* package */ ProgressiveMediaSource(
MediaItem mediaItem,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
int continueLoadingCheckIntervalBytes) {
this.playbackProperties = checkNotNull(mediaItem.playbackProperties);
this.mediaItem = mediaItem;
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
this.drmSessionManager = drmSessionManager;
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
@ -308,7 +321,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return new ProgressiveMediaPeriod(
playbackProperties.uri,
dataSource,
extractorsFactory,
progressiveMediaExtractorFactory.createProgressiveMediaExtractor(),
drmSessionManager,
createDrmEventDispatcher(id),
loadableLoadErrorHandlingPolicy,

View File

@ -29,8 +29,12 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
import com.google.android.exoplayer2.upstream.DataReader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -41,6 +45,41 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtractor {
/** {@link ChunkExtractor.Factory} for instances of this class. */
public static final ChunkExtractor.Factory FACTORY =
(primaryTrackType,
format,
enableEventMessageTrack,
closedCaptionFormats,
playerEmsgTrackOutput) -> {
@Nullable String containerMimeType = format.containerMimeType;
Extractor extractor;
if (MimeTypes.isText(containerMimeType)) {
if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
// RawCC is special because it's a text specific container format.
extractor = new RawCcExtractor(format);
} else {
// All other text types are raw formats that do not need an extractor.
return null;
}
} else if (MimeTypes.isMatroska(containerMimeType)) {
extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);
} else {
int flags = 0;
if (enableEventMessageTrack) {
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
}
extractor =
new FragmentedMp4Extractor(
flags,
/* timestampAdjuster= */ null,
/* sideloadedTrack= */ null,
closedCaptionFormats,
playerEmsgTrackOutput);
}
return new BundledChunkExtractor(extractor, primaryTrackType, format);
};
private static final PositionHolder POSITION_HOLDER = new PositionHolder();
private final Extractor extractor;

View File

@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import java.io.IOException;
import java.util.List;
/**
* Extracts samples and track {@link Format Formats} from chunks.
@ -31,6 +32,27 @@ import java.io.IOException;
*/
public interface ChunkExtractor {
/** Creates {@link ChunkExtractor} instances. */
interface Factory {
/**
* Returns a new {@link ChunkExtractor} instance.
*
* @param primaryTrackType The type of the primary track. One of {@link C C.TRACK_TYPE_*}.
* @param representationFormat The format of the representation to extract from.
* @param enableEventMessageTrack Whether to enable the event message track.
* @param closedCaptionFormats The {@link Format Formats} of the Closed-Caption tracks.
* @return A new {@link ChunkExtractor} instance, or null if not applicable.
*/
@Nullable
ChunkExtractor createProgressiveMediaExtractor(
int primaryTrackType,
Format representationFormat,
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput);
}
/** Provides {@link TrackOutput} instances to be written to during extraction. */
interface TrackOutputProvider {

View File

@ -26,6 +26,7 @@ import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.P
import android.annotation.SuppressLint;
import android.media.MediaFormat;
import android.media.MediaParser;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
@ -49,6 +50,25 @@ import java.util.List;
@RequiresApi(30)
public final class MediaParserChunkExtractor implements ChunkExtractor {
// Maximum TAG length is 23 characters.
private static final String TAG = "MediaPrsrChunkExtractor";
public static final ChunkExtractor.Factory FACTORY =
(primaryTrackType,
format,
enableEventMessageTrack,
closedCaptionFormats,
playerEmsgTrackOutput) -> {
if (!MimeTypes.isText(format.containerMimeType)) {
// Container is either Matroska or Fragmented MP4.
return new MediaParserChunkExtractor(primaryTrackType, format, closedCaptionFormats);
} else {
// This is either RAWCC (unsupported) or a text track that does not require an extractor.
Log.w(TAG, "Ignoring an unsupported text track.");
return null;
}
};
private final OutputConsumerAdapterV30 outputConsumerAdapter;
private final InputReaderAdapterV30 inputReaderAdapter;
private final MediaParser mediaParser;

View File

@ -318,8 +318,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
}
if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) {
cue.setTextSize(
style.fontSize / screenHeight,
Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
}
if (style.bold && style.italic) {
spannableText.setSpan(

View File

@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util;
break;
}
}
return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET)
return (startTimeIndex != C.INDEX_UNSET
&& endTimeIndex != C.INDEX_UNSET
&& textIndex != C.INDEX_UNSET)
? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length)
: null;
}

View File

@ -125,11 +125,21 @@ import java.util.regex.Pattern;
try {
return new SsaStyle(
styleValues[format.nameIndex].trim(),
parseAlignment(styleValues[format.alignmentIndex].trim()),
parseColor(styleValues[format.primaryColorIndex].trim()),
parseFontSize(styleValues[format.fontSizeIndex].trim()),
parseBold(styleValues[format.boldIndex].trim()),
parseItalic(styleValues[format.italicIndex].trim()));
format.alignmentIndex != C.INDEX_UNSET
? parseAlignment(styleValues[format.alignmentIndex].trim())
: SSA_ALIGNMENT_UNKNOWN,
format.primaryColorIndex != C.INDEX_UNSET
? parseColor(styleValues[format.primaryColorIndex].trim())
: null,
format.fontSizeIndex != C.INDEX_UNSET
? parseFontSize(styleValues[format.fontSizeIndex].trim())
: Cue.DIMEN_UNSET,
format.boldIndex != C.INDEX_UNSET)
? parseBold(styleValues[format.boldIndex].trim())
: false,
format.italicIndex != C.INDEX_UNSET)
? parseItalic(styleValues[format.italicIndex].trim())
: false);
} catch (RuntimeException e) {
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
return null;

View File

@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition;
import com.google.android.exoplayer2.util.MimeTypes;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Track selection related utility methods. */
@ -97,4 +98,20 @@ public final class TrackSelectionUtil {
}
return builder.build();
}
/** Returns if a {@link TrackSelectionArray} has at least one track of the given type. */
public static boolean hasTrackOfType(TrackSelectionArray trackSelections, int trackType) {
for (int i = 0; i < trackSelections.length; i++) {
@Nullable TrackSelection trackSelection = trackSelections.get(i);
if (trackSelection == null) {
continue;
}
for (int j = 0; j < trackSelection.length(); j++) {
if (MimeTypes.getTrackType(trackSelection.getFormat(j).sampleMimeType) == trackType) {
return true;
}
}
}
return false;
}
}

View File

@ -71,7 +71,7 @@ public final class AssetDataSource extends BaseDataSource {
if (skipped < dataSpec.position) {
// assetManager.open() returns an AssetInputStream, whose skip() implementation only skips
// fewer bytes than requested if the skip is beyond the end of the asset's data.
throw new EOFException();
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;

View File

@ -47,13 +47,13 @@ public final class ByteArrayDataSource extends BaseDataSource {
public long open(DataSpec dataSpec) throws IOException {
uri = dataSpec.uri;
transferInitializing(dataSpec);
readPosition = (int) dataSpec.position;
bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET)
? (data.length - dataSpec.position) : dataSpec.length);
if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) {
throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length
+ "], length: " + data.length);
if (dataSpec.position >= data.length) {
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
readPosition = (int) dataSpec.position;
bytesRemaining =
(int)
(dataSpec.length == C.LENGTH_UNSET ? data.length - dataSpec.position : dataSpec.length);
opened = true;
transferStarted(dataSpec);
return bytesRemaining;

View File

@ -80,7 +80,7 @@ public final class ContentDataSource extends BaseDataSource {
if (skipped != dataSpec.position) {
// We expect the skip to be satisfied in full. If it isn't then we're probably trying to
// skip beyond the end of the data.
throw new EOFException();
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
@ -96,13 +96,13 @@ public final class ContentDataSource extends BaseDataSource {
} else {
bytesRemaining = channelSize - channel.position();
if (bytesRemaining < 0) {
throw new EOFException();
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
}
} else {
bytesRemaining = assetFileDescriptorLength - skipped;
if (bytesRemaining < 0) {
throw new EOFException();
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
}
}

View File

@ -69,7 +69,7 @@ public final class DataSchemeDataSource extends BaseDataSource {
}
endPosition =
dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;
if (endPosition > data.length || readPosition > endPosition) {
if (readPosition >= endPosition) {
data = null;
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}

View File

@ -23,7 +23,6 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
@ -91,7 +90,7 @@ public final class FileDataSource extends BaseDataSource {
bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position
: dataSpec.length;
if (bytesRemaining < 0) {
throw new EOFException();
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
} catch (IOException e) {
throw new FileDataSourceException(e);

View File

@ -60,7 +60,7 @@ public final class RawResourceDataSource extends BaseDataSource {
super(message);
}
public RawResourceDataSourceException(IOException e) {
public RawResourceDataSourceException(Throwable e) {
super(e);
}
}
@ -133,21 +133,39 @@ public final class RawResourceDataSource extends BaseDataSource {
}
transferInitializing(dataSpec);
AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId);
AssetFileDescriptor assetFileDescriptor;
try {
assetFileDescriptor = resources.openRawResourceFd(resourceId);
} catch (Resources.NotFoundException e) {
throw new RawResourceDataSourceException(e);
}
this.assetFileDescriptor = assetFileDescriptor;
if (assetFileDescriptor == null) {
throw new RawResourceDataSourceException("Resource is compressed: " + uri);
}
long assetFileDescriptorLength = assetFileDescriptor.getLength();
FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
this.inputStream = inputStream;
try {
// We can't rely only on the "skipped < dataSpec.position" check below to detect whether the
// position is beyond the end of the resource being read. This is because the file will
// typically contain multiple resources, and there's nothing to prevent InputStream.skip()
// from succeeding by skipping into the data of the next resource. Hence we also need to check
// against the resource length explicitly, which is guaranteed to be set unless the resource
// extends to the end of the file.
if (assetFileDescriptorLength != AssetFileDescriptor.UNKNOWN_LENGTH
&& dataSpec.position > assetFileDescriptorLength) {
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
inputStream.skip(assetFileDescriptor.getStartOffset());
long skipped = inputStream.skip(dataSpec.position);
if (skipped < dataSpec.position) {
// We expect the skip to be satisfied in full. If it isn't then we're probably trying to
// skip beyond the end of the data.
throw new EOFException();
// read beyond the end of the last resource in the file.
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
} catch (IOException e) {
throw new RawResourceDataSourceException(e);
@ -156,7 +174,6 @@ public final class RawResourceDataSource extends BaseDataSource {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
long assetFileDescriptorLength = assetFileDescriptor.getLength();
// If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
bytesRemaining =
assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH

View File

@ -55,7 +55,6 @@ public final class CacheWriter {
private final byte[] temporaryBuffer;
@Nullable private final ProgressListener progressListener;
private boolean initialized;
private long nextPosition;
private long endPosition;
private long bytesCached;
@ -118,18 +117,15 @@ public final class CacheWriter {
public void cache() throws IOException {
throwIfCanceled();
if (!initialized) {
if (dataSpec.length != C.LENGTH_UNSET) {
endPosition = dataSpec.position + dataSpec.length;
} else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey));
endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength;
}
bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length);
if (progressListener != null) {
progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0);
}
initialized = true;
bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length);
if (dataSpec.length != C.LENGTH_UNSET) {
endPosition = dataSpec.position + dataSpec.length;
} else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey));
endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength;
}
if (progressListener != null) {
progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0);
}
while (endPosition == C.POSITION_UNSET || nextPosition < endPosition) {
@ -158,42 +154,50 @@ public final class CacheWriter {
*/
private long readBlockToCache(long position, long length) throws IOException {
boolean isLastBlock = position + length == endPosition || length == C.LENGTH_UNSET;
try {
long resolvedLength = C.LENGTH_UNSET;
boolean isDataSourceOpen = false;
if (length != C.LENGTH_UNSET) {
// If the length is specified, try to open the data source with a bounded request to avoid
// the underlying network stack requesting more data than required.
try {
DataSpec boundedDataSpec =
dataSpec.buildUpon().setPosition(position).setLength(length).build();
resolvedLength = dataSource.open(boundedDataSpec);
isDataSourceOpen = true;
} catch (IOException exception) {
if (allowShortContent
&& isLastBlock
&& DataSourceException.isCausedByPositionOutOfRange(exception)) {
// The length of the request exceeds the length of the content. If we allow shorter
// content and are reading the last block, fall through and try again with an unbounded
// request to read up to the end of the content.
Util.closeQuietly(dataSource);
} else {
throw exception;
}
long resolvedLength = C.LENGTH_UNSET;
boolean isDataSourceOpen = false;
if (length != C.LENGTH_UNSET) {
// If the length is specified, try to open the data source with a bounded request to avoid
// the underlying network stack requesting more data than required.
DataSpec boundedDataSpec =
dataSpec.buildUpon().setPosition(position).setLength(length).build();
try {
resolvedLength = dataSource.open(boundedDataSpec);
isDataSourceOpen = true;
} catch (IOException e) {
Util.closeQuietly(dataSource);
if (allowShortContent
&& isLastBlock
&& DataSourceException.isCausedByPositionOutOfRange(e)) {
// The length of the request exceeds the length of the content. If we allow shorter
// content and are reading the last block, fall through and try again with an unbounded
// request to read up to the end of the content.
} else {
throw e;
}
}
if (!isDataSourceOpen) {
// Either the length was unspecified, or we allow short content and our attempt to open the
// DataSource with the specified length failed.
throwIfCanceled();
DataSpec unboundedDataSpec =
dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build();
}
if (!isDataSourceOpen) {
// Either the length was unspecified, or we allow short content and our attempt to open the
// DataSource with the specified length failed.
throwIfCanceled();
DataSpec unboundedDataSpec =
dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build();
try {
resolvedLength = dataSource.open(unboundedDataSpec);
} catch (IOException e) {
Util.closeQuietly(dataSource);
throw e;
}
}
int totalBytesRead = 0;
try {
if (isLastBlock && resolvedLength != C.LENGTH_UNSET) {
onRequestEndPosition(position + resolvedLength);
}
int totalBytesRead = 0;
int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) {
throwIfCanceled();
@ -206,10 +210,16 @@ public final class CacheWriter {
if (isLastBlock) {
onRequestEndPosition(position + totalBytesRead);
}
return totalBytesRead;
} finally {
} catch (IOException e) {
Util.closeQuietly(dataSource);
throw e;
}
// Util.closeQuietly(dataSource) is not used here because it's important that an exception is
// thrown if DataSource.close fails. This is because there's no way of knowing whether the block
// was successfully cached in this case.
dataSource.close();
return totalBytesRead;
}
private void onRequestEndPosition(long endPosition) {

View File

@ -35,6 +35,7 @@ import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
@ -479,8 +480,8 @@ public class EventLogger implements AnalyticsListener {
}
@Override
public void onDrmSessionAcquired(EventTime eventTime) {
logd(eventTime, "drmSessionAcquired");
public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {
logd(eventTime, "drmSessionAcquired", "state=" + state);
}
@Override

View File

@ -69,11 +69,8 @@ public interface VideoRendererEventListener {
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@SuppressWarnings("deprecation")
default void onVideoInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
onVideoInputFormatChanged(format);
}
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
/**
* Called to report the number of frames dropped by the renderer. Dropped frames are reported
@ -133,7 +130,12 @@ public interface VideoRendererEventListener {
*
* @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
* the renderer renders to something that isn't a {@link Surface}.
* @param renderTimeMs The {@link SystemClock#elapsedRealtime()} when the frame was rendered.
*/
default void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {}
/** @deprecated Use {@link #onRenderedFirstFrame(Surface, long)}. */
@Deprecated
default void onRenderedFirstFrame(@Nullable Surface surface) {}
/**
@ -205,11 +207,15 @@ public interface VideoRendererEventListener {
* Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format,
* DecoderReuseEvaluation)}.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method.
public void inputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
if (handler != null) {
handler.post(
() -> castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation));
() -> {
castNonNull(listener).onVideoInputFormatChanged(format);
castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation);
});
}
}
@ -245,10 +251,16 @@ public interface VideoRendererEventListener {
}
}
/** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */
/** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface, long)}. */
public void renderedFirstFrame(@Nullable Surface surface) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface));
// TODO: Replace this timestamp with the actual frame release time.
long renderTimeMs = SystemClock.elapsedRealtime();
handler.post(
() -> {
castNonNull(listener).onRenderedFirstFrame(surface);
castNonNull(listener).onRenderedFirstFrame(surface, renderTimeMs);
});
}
}

View File

@ -8605,6 +8605,20 @@ public final class ExoPlayerTest {
assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L));
}
@Test
public void playerIdle_withSetPlaybackSpeed_usesPlaybackParameterSpeedWithPitchUnchanged() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1, /* pitch= */ 2));
Player.EventListener mockListener = mock(Player.EventListener.class);
player.addListener(mockListener);
player.prepare();
player.setPlaybackSpeed(2);
verify(mockListener)
.onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2, /* pitch= */ 2));
}
@Test
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
throws Exception {

View File

@ -80,6 +80,7 @@ import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaDrm;
import com.google.android.exoplayer2.drm.MediaDrmCallback;
@ -1700,12 +1701,12 @@ public final class AnalyticsCollectorTest {
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
.onVideoDecoderInitialized(
individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong());
individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong());
ArgumentCaptor<AnalyticsListener.EventTime> individualAudioDecoderInitializedEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
.onAudioDecoderInitialized(
individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong());
individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong());
ArgumentCaptor<AnalyticsListener.EventTime> individualVideoDisabledEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
@ -1717,7 +1718,7 @@ public final class AnalyticsCollectorTest {
ArgumentCaptor<AnalyticsListener.EventTime> individualRenderedFirstFrameEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
.onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any());
.onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any(), anyLong());
ArgumentCaptor<AnalyticsListener.EventTime> individualVideoSizeChangedEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
@ -2183,7 +2184,10 @@ public final class AnalyticsCollectorTest {
@Override
public void onAudioDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_DECODER_INITIALIZED, eventTime));
}
@ -2220,7 +2224,10 @@ public final class AnalyticsCollectorTest {
@Override
public void onVideoDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {
reportedEvents.add(new ReportedEvent(EVENT_VIDEO_DECODER_INITIALIZED, eventTime));
}
@ -2246,7 +2253,8 @@ public final class AnalyticsCollectorTest {
}
@Override
public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {
public void onRenderedFirstFrame(
EventTime eventTime, @Nullable Surface surface, long renderTimeMs) {
reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime));
}
@ -2261,7 +2269,7 @@ public final class AnalyticsCollectorTest {
}
@Override
public void onDrmSessionAcquired(EventTime eventTime) {
public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {
reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_ACQUIRED, eventTime));
}

View File

@ -66,7 +66,7 @@ public final class DefaultAudioSinkTest {
new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor),
/* enableFloatOutput= */ false,
/* enableAudioTrackPlaybackParams= */ false,
/* enableOffload= */ false);
DefaultAudioSink.OFFLOAD_MODE_DISABLED);
}
@Test

View File

@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper;
// - Multiple acquisitions & releases for same keys -> multiple requests.
// - Provisioning.
// - Key denial.
// - Handling of ResourceBusyException (indicating session scarcity).
@RunWith(AndroidJUnit4.class)
public class DefaultDrmSessionManagerTest {
@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest {
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
}
@Test(timeout = 10_000)
public void preacquireSession_loadsKeysBeforeFullAcquisition() throws Exception {
AtomicInteger keyLoadCount = new AtomicInteger(0);
DrmSessionEventListener.EventDispatcher eventDispatcher =
new DrmSessionEventListener.EventDispatcher();
eventDispatcher.addEventListener(
Util.createHandlerForCurrentLooper(),
new DrmSessionEventListener() {
@Override
public void onDrmKeysLoaded(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
keyLoadCount.incrementAndGet();
}
});
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
// Disable keepalive
.setSessionKeepaliveMs(C.TIME_UNSET)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSessionManager.DrmSessionReference sessionReference =
drmSessionManager.preacquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
eventDispatcher,
FORMAT_WITH_DRM_INIT_DATA);
// Wait for the key load event to propagate, indicating the pre-acquired session is in
// STATE_OPENED_WITH_KEYS.
while (keyLoadCount.get() == 0) {
// Allow the key response to be handled.
ShadowLooper.idleMainLooper();
}
DrmSession drmSession =
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
// Without idling the main/playback looper, we assert the session is already in OPENED_WITH_KEYS
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
assertThat(keyLoadCount.get()).isEqualTo(1);
// After releasing our concrete session reference, the session is held open by the pre-acquired
// reference.
drmSession.release(/* eventDispatcher= */ null);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
// Releasing the pre-acquired reference allows the session to be fully released.
sessionReference.release();
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
}
@Test(timeout = 10_000)
public void
preacquireSession_releaseBeforeUnderlyingAcquisitionCompletesReleasesSessionOnceAcquired()
throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
// Disable keepalive
.setSessionKeepaliveMs(C.TIME_UNSET)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSessionManager.DrmSessionReference sessionReference =
drmSessionManager.preacquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA);
// Release the pre-acquired reference before the underlying session has had a chance to be
// constructed.
sessionReference.release();
// Acquiring the same session triggers a second key load (because the pre-acquired session was
// fully released).
DrmSession drmSession =
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED);
waitForOpenedWithKeys(drmSession);
drmSession.release(/* eventDispatcher= */ null);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
}
@Test(timeout = 10_000)
public void preacquireSession_releaseManagerBeforeAcquisition_acquisitionDoesntHappen()
throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
// Disable keepalive
.setSessionKeepaliveMs(C.TIME_UNSET)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSessionManager.DrmSessionReference sessionReference =
drmSessionManager.preacquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA);
// Release the manager before the underlying session has had a chance to be constructed. This
// will release all pre-acquired sessions.
drmSessionManager.release();
// Allow the acquisition event to be handled on the main/playback thread.
ShadowLooper.idleMainLooper();
// Re-prepare the manager so we can fully acquire the same session, and check the previous
// pre-acquisition didn't do anything.
drmSessionManager.prepare();
DrmSession drmSession =
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED);
waitForOpenedWithKeys(drmSession);
drmSession.release(/* eventDispatcher= */ null);
// If the (still unreleased) pre-acquired session above was linked to the same underlying
// session then the state would still be OPENED_WITH_KEYS.
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
// Release the pre-acquired session from above (this is a no-op, but we do it anyway for
// correctness).
sessionReference.release();
drmSessionManager.release();
}
private static void waitForOpenedWithKeys(DrmSession drmSession) {
// Check the error first, so we get a meaningful failure if there's been an error.
assertThat(drmSession.getError()).isNull();

View File

@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.testutil.FailOnCloseDataSink;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
@ -34,6 +35,7 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -66,7 +68,7 @@ public class ProgressiveDownloaderTest {
}
@Test
public void download_afterSingleFailure_succeeds() throws Exception {
public void download_afterReadFailure_succeeds() throws Exception {
Uri uri = Uri.parse("test:///test.mp4");
// Fake data has a built in failure after 10 bytes.
@ -92,6 +94,39 @@ public class ProgressiveDownloaderTest {
assertThat(progressListener.bytesDownloaded).isEqualTo(30);
}
@Test
public void download_afterWriteFailureOnClose_succeeds() throws Exception {
Uri uri = Uri.parse("test:///test.mp4");
FakeDataSet data = new FakeDataSet();
data.newData(uri).appendReadData(1024);
DataSource.Factory upstreamDataSource = new FakeDataSource.Factory().setFakeDataSet(data);
AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true);
FailOnCloseDataSink.Factory dataSinkFactory =
new FailOnCloseDataSink.Factory(downloadCache, failOnClose);
MediaItem mediaItem = MediaItem.fromUri(uri);
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(downloadCache)
.setCacheWriteDataSinkFactory(dataSinkFactory)
.setUpstreamDataSourceFactory(upstreamDataSource);
ProgressiveDownloader downloader = new ProgressiveDownloader(mediaItem, cacheDataSourceFactory);
TestProgressListener progressListener = new TestProgressListener();
// Failure expected after 1024 bytes.
assertThrows(IOException.class, () -> downloader.download(progressListener));
assertThat(progressListener.bytesDownloaded).isEqualTo(1024);
failOnClose.set(false);
// Retry should succeed.
downloader.download(progressListener);
assertThat(progressListener.bytesDownloaded).isEqualTo(1024);
}
private static final class TestProgressListener implements Downloader.ProgressListener {
public long bytesDownloaded;

View File

@ -24,22 +24,37 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.upstream.AssetDataSource;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** Unit test for {@link ProgressiveMediaPeriod}. */
@RunWith(AndroidJUnit4.class)
public final class ProgressiveMediaPeriodTest {
@Test
public void prepare_updatesSourceInfoBeforeOnPreparedCallback() throws Exception {
public void prepareUsingBundledExtractors_updatesSourceInfoBeforeOnPreparedCallback()
throws TimeoutException {
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
new BundledExtractorsAdapter(Mp4Extractor.FACTORY));
}
@Test
@Config(sdk = 30)
public void prepareUsingMediaParser_updatesSourceInfoBeforeOnPreparedCallback()
throws TimeoutException {
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(new MediaParserExtractorAdapter());
}
private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
ProgressiveMediaExtractor extractor) throws TimeoutException {
AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false);
ProgressiveMediaPeriod.Listener sourceInfoRefreshListener =
(durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true);
@ -48,7 +63,7 @@ public final class ProgressiveMediaPeriodTest {
new ProgressiveMediaPeriod(
Uri.parse("asset://android_asset/media/mp4/sample.mp4"),
new AssetDataSource(ApplicationProvider.getApplicationContext()),
() -> new Extractor[] {new Mp4Extractor()},
extractor,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),

View File

@ -323,17 +323,18 @@ public final class SsaDecoderTest {
}
@Test
public void decodeFontSize() throws IOException{
public void decodeFontSize() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.textSize).isEqualTo(30f/720f);
assertThat(firstCue.textSize).isWithin(1.0e-8f).of(30f / 720f);
assertThat(firstCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
assertThat(secondCue.textSize).isEqualTo(72.2f/720f);
assertThat(secondCue.textSize).isWithin(1.0e-8f).of(72.2f / 720f);
assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
}

View File

@ -87,14 +87,6 @@ public final class ByteArrayDataSourceTest {
readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true);
}
@Test
public void readWithInvalidLength() {
// Read more data than is available.
readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true);
// And with bound.
readTestData(TEST_DATA, 1, TEST_DATA.length, 1, 0, 1, true);
}
/**
* Tests reading from a {@link ByteArrayDataSource} with various parameters.
*

View File

@ -50,7 +50,6 @@ public class CacheDataSourceContractTest extends DataSourceContractTest {
File file = tempFolder.newFile();
Files.write(Paths.get(file.getAbsolutePath()), DATA);
simpleUri = Uri.fromFile(file);
fileDataSource = new FileDataSource();
}
@Override
@ -74,6 +73,7 @@ public class CacheDataSourceContractTest extends DataSourceContractTest {
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
SimpleCache cache =
new SimpleCache(tempFolder, new NoOpCacheEvictor(), TestUtil.getInMemoryDatabaseProvider());
fileDataSource = new FileDataSource();
return new CacheDataSource(cache, fileDataSource);
}

View File

@ -39,12 +39,12 @@ public class DataSchemeDataSourceContractTest extends DataSourceContractTest {
return ImmutableList.of(
new TestResource.Builder()
.setName("plain text")
.setUri(Uri.parse("data:text/plain," + DATA))
.setUri("data:text/plain," + DATA)
.setExpectedBytes(DATA.getBytes(UTF_8))
.build(),
new TestResource.Builder()
.setName("base64 encoded text")
.setUri(Uri.parse("data:text/plain;base64," + BASE64_ENCODED_DATA))
.setUri("data:text/plain;base64," + BASE64_ENCODED_DATA)
.setExpectedBytes(Base64.decode(BASE64_ENCODED_DATA, Base64.DEFAULT))
.build());
}

View File

@ -107,18 +107,6 @@ public final class DataSchemeDataSourceTest {
}
}
@Test
public void rangeExceedingResourceLengthRequest() throws IOException {
try {
// Try to open a range exceeding the resource's length.
schemeDataDataSource.open(
buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11));
fail();
} catch (DataSourceException e) {
assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE);
}
}
@Test
public void incorrectScheme() {
try {

View File

@ -0,0 +1,83 @@
/*
* Copyright 2021 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.upstream;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.ResolvingDataSource.Resolver;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import org.junit.Before;
import org.junit.runner.RunWith;
/** {@link DataSource} contract tests for {@link ResolvingDataSourceContractTest}. */
@RunWith(AndroidJUnit4.class)
public class ResolvingDataSourceContractTest extends DataSourceContractTest {
private static final String URI = "test://simple.test";
private static final String RESOLVED_URI = "resolved://simple.resolved";
private byte[] simpleData;
private FakeDataSet fakeDataSet;
private FakeDataSource fakeDataSource;
@Before
public void setUp() {
simpleData = TestUtil.buildTestData(/* length= */ 20);
fakeDataSet = new FakeDataSet().newData(RESOLVED_URI).appendReadData(simpleData).endData();
}
@Override
protected ImmutableList<TestResource> getTestResources() {
return ImmutableList.of(
new TestResource.Builder()
.setName("simple")
.setUri(URI)
.setExpectedBytes(simpleData)
.build());
}
@Override
protected Uri getNotFoundUri() {
return Uri.parse("test://not-found.test");
}
@Override
protected DataSource createDataSource() {
fakeDataSource = new FakeDataSource(fakeDataSet);
return new ResolvingDataSource(
fakeDataSource,
new Resolver() {
@Override
public DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException {
return URI.equals(dataSpec.uri.toString())
? dataSpec.buildUpon().setUri(RESOLVED_URI).build()
: dataSpec;
}
});
}
@Override
@Nullable
protected DataSource getTransferListenerDataSource() {
return fakeDataSource;
}
}

View File

@ -53,14 +53,18 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
return udpDataSource;
}
@Override
protected boolean unboundedReadsAreIndefinite() {
return true;
}
@Override
protected ImmutableList<TestResource> getTestResources() {
return ImmutableList.of(
new TestResource.Builder()
.setName("local-udp-unicast-socket")
.setUri(Uri.parse("udp://localhost:" + findFreeUdpPort()))
.setUri("udp://localhost:" + findFreeUdpPort())
.setExpectedBytes(data)
.setEndOfInputExpected(false)
.build());
}
@ -84,6 +88,26 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
@Override
public void dataSpecWithPositionAndLength_readExpectedRange() {}
@Test
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
@Override
public void dataSpecWithPositionAtEnd_throwsPositionOutOfRangeException() {}
@Test
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
@Override
public void dataSpecWithPositionAtEndAndLength_throwsPositionOutOfRangeException() {}
@Test
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
@Override
public void dataSpecWithPositionOutOfRange_throwsPositionOutOfRangeException() {}
@Test
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
@Override
public void dataSpecWithEndPositionOutOfRange_readsToEnd() {}
/**
* Finds a free UDP port in the range of unreserved ports 50000-60000 that can be used from the
* test or throws an {@link IllegalStateException} if no port is available.

View File

@ -17,84 +17,40 @@ package com.google.android.exoplayer2.upstream.cache;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FailOnCloseDataSink;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Unit tests for {@link CacheWriter}. */
@RunWith(AndroidJUnit4.class)
public final class CacheWriterTest {
/**
* Abstract fake Cache implementation used by the test. This class must be public so Mockito can
* create a proxy for it.
*/
public abstract static class AbstractFakeCache implements Cache {
// This array is set to alternating length of cached and not cached regions in tests:
// spansAndGaps = {<length of 1st cached region>, <length of 1st not cached region>,
// <length of 2nd cached region>, <length of 2nd not cached region>, ... }
// Ideally it should end with a cached region but it shouldn't matter for any code.
private int[] spansAndGaps;
private long contentLength;
private void init() {
spansAndGaps = new int[] {};
contentLength = C.LENGTH_UNSET;
}
@Override
public long getCachedLength(String key, long position, long length) {
if (length == C.LENGTH_UNSET) {
length = Long.MAX_VALUE;
}
for (int i = 0; i < spansAndGaps.length; i++) {
int spanOrGap = spansAndGaps[i];
if (position < spanOrGap) {
long left = min(spanOrGap - position, length);
return (i & 1) == 1 ? -left : left;
}
position -= spanOrGap;
}
return -length;
}
@Override
public ContentMetadata getContentMetadata(String key) {
DefaultContentMetadata metadata = new DefaultContentMetadata();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, contentLength);
return metadata.copyWithMutationsApplied(mutations);
}
}
@Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache;
private File tempFolder;
private SimpleCache cache;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockCache.init();
tempFolder =
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cache =
@ -219,6 +175,7 @@ public final class CacheWriterTest {
assertCachedData(cache, fakeDataSet);
}
@Ignore("Currently broken. See https://github.com/google/ExoPlayer/issues/7326.")
@Test
public void cacheLengthExceedsActualDataLength() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
@ -263,6 +220,50 @@ public final class CacheWriterTest {
assertThat(DataSourceException.isCausedByPositionOutOfRange(exception)).isTrue();
}
@Test
public void cache_afterFailureOnClose_succeeds() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource upstreamDataSource = new FakeDataSource(fakeDataSet);
AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true);
FailOnCloseDataSink dataSink = new FailOnCloseDataSink(cache, failOnClose);
CacheDataSource cacheDataSource =
new CacheDataSource(
cache,
upstreamDataSource,
new FileDataSource(),
dataSink,
/* flags= */ 0,
/* eventListener= */ null);
CachingCounters counters = new CachingCounters();
CacheWriter cacheWriter =
new CacheWriter(
cacheDataSource,
new DataSpec(Uri.parse("test_data")),
/* allowShortContent= */ false,
/* temporaryBuffer= */ null,
counters);
// DataSink.close failing must cause the operation to fail rather than succeed.
assertThrows(IOException.class, cacheWriter::cache);
// Since all of the bytes were read through the DataSource chain successfully before the sink
// was closed, the progress listener will have seen all of the bytes being cached, even though
// this may not really be the case.
counters.assertValues(
/* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100);
failOnClose.set(false);
// The bytes will be downloaded again, but cached successfully this time.
cacheWriter.cache();
counters.assertValues(
/* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cachePolling() throws Exception {
final CachingCounters counters = new CachingCounters();

View File

@ -26,11 +26,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor;
@ -53,7 +48,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
@ -180,11 +174,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
representationHolders[i] =
new RepresentationHolder(
periodDurationUs,
trackType,
representation,
enableEventMessageTrack,
closedCaptionFormats,
playerTrackEmsgHandler);
BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor(
trackType,
representation.format,
enableEventMessageTrack,
closedCaptionFormats,
playerTrackEmsgHandler),
/* segmentNumShift= */ 0,
representation.getIndex());
}
}
@ -665,26 +663,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
private final long segmentNumShift;
/* package */ RepresentationHolder(
long periodDurationUs,
int trackType,
Representation representation,
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput) {
this(
periodDurationUs,
representation,
createChunkExtractor(
trackType,
representation,
enableEventMessageTrack,
closedCaptionFormats,
playerEmsgTrackOutput),
/* segmentNumShift= */ 0,
representation.getIndex());
}
private RepresentationHolder(
long periodDurationUs,
Representation representation,
@Nullable ChunkExtractor chunkExtractor,
@ -800,40 +778,5 @@ public class DefaultDashChunkSource implements DashChunkSource {
public boolean isSegmentAvailableAtFullNetworkSpeed(long segmentNum, long nowPeriodTimeUs) {
return nowPeriodTimeUs == C.TIME_UNSET || getSegmentEndTimeUs(segmentNum) <= nowPeriodTimeUs;
}
@Nullable
private static ChunkExtractor createChunkExtractor(
int trackType,
Representation representation,
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput) {
String containerMimeType = representation.format.containerMimeType;
Extractor extractor;
if (MimeTypes.isText(containerMimeType)) {
if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
// RawCC is special because it's a text specific container format.
extractor = new RawCcExtractor(representation.format);
} else {
// All other text types are raw formats that do not need an extractor.
return null;
}
} else if (MimeTypes.isMatroska(containerMimeType)) {
extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);
} else {
int flags = 0;
if (enableEventMessageTrack) {
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
}
extractor =
new FragmentedMp4Extractor(
flags,
/* timestampAdjuster= */ null,
/* sideloadedTrack= */ null,
closedCaptionFormats,
playerEmsgTrackOutput);
}
return new BundledChunkExtractor(extractor, trackType, representation.format);
}
}
}

View File

@ -913,11 +913,12 @@ public class PlayerControlView extends FrameLayout {
timeline.getWindow(player.getCurrentWindowIndex(), window);
boolean isSeekable = window.isSeekable;
enableSeeking = isSeekable;
enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();
enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
enableNext =
window.isLive() || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
(window.isLive() && window.isDynamic)
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
}

View File

@ -320,6 +320,7 @@ public class PlayerNotificationManager {
private int fastForwardActionIconResourceId;
private int previousActionIconResourceId;
private int nextActionIconResourceId;
@Nullable private String groupKey;
/**
* Creates an instance.
@ -514,6 +515,18 @@ public class PlayerNotificationManager {
return this;
}
/**
* The key of the group the media notification should belong to.
*
* <p>The default is {@code null}
*
* @return This builder.
*/
public Builder setGroup(String groupKey) {
this.groupKey = groupKey;
return this;
}
/** Builds the {@link PlayerNotificationManager}. */
public PlayerNotificationManager build() {
if (channelNameResourceId != 0) {
@ -538,7 +551,8 @@ public class PlayerNotificationManager {
rewindActionIconResourceId,
fastForwardActionIconResourceId,
previousActionIconResourceId,
nextActionIconResourceId);
nextActionIconResourceId,
groupKey);
}
}
@ -662,6 +676,7 @@ public class PlayerNotificationManager {
private int visibility;
@Priority private int priority;
private boolean useChronometer;
@Nullable private String groupKey;
/** @deprecated Use the {@link Builder} instead. */
@SuppressWarnings("deprecation")
@ -805,7 +820,8 @@ public class PlayerNotificationManager {
R.drawable.exo_notification_rewind,
R.drawable.exo_notification_fastforward,
R.drawable.exo_notification_previous,
R.drawable.exo_notification_next);
R.drawable.exo_notification_next,
null);
}
private PlayerNotificationManager(
@ -822,7 +838,8 @@ public class PlayerNotificationManager {
int rewindActionIconResourceId,
int fastForwardActionIconResourceId,
int previousActionIconResourceId,
int nextActionIconResourceId) {
int nextActionIconResourceId,
@Nullable String groupKey) {
context = context.getApplicationContext();
this.context = context;
this.channelId = channelId;
@ -831,6 +848,7 @@ public class PlayerNotificationManager {
this.notificationListener = notificationListener;
this.customActionReceiver = customActionReceiver;
this.smallIconResourceId = smallIconResourceId;
this.groupKey = groupKey;
controlDispatcher = new DefaultControlDispatcher();
window = new Timeline.Window();
instanceId = instanceIdCounter++;
@ -1407,6 +1425,10 @@ public class PlayerNotificationManager {
setLargeIcon(builder, largeIcon);
builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player));
if (groupKey != null) {
builder.setGroup(groupKey);
}
return builder;
}
@ -1437,10 +1459,13 @@ public class PlayerNotificationManager {
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && !player.isPlayingAd()) {
timeline.getWindow(player.getCurrentWindowIndex(), window);
enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();
enableRewind = controlDispatcher.isRewindEnabled();
enableFastForward = controlDispatcher.isFastForwardEnabled();
enableNext = window.isDynamic || player.hasNext();
boolean isSeekable = window.isSeekable;
enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
enableNext =
(window.isLive() && window.isDynamic)
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
List<String> stringActions = new ArrayList<>();

View File

@ -58,6 +58,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionUtil;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.spherical.SingleTapListener;
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
@ -1332,14 +1333,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
closeShutter();
}
TrackSelectionArray selections = player.getCurrentTrackSelections();
for (int i = 0; i < selections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
// onRenderedFirstFrame().
hideArtwork();
return;
}
if (TrackSelectionUtil.hasTrackOfType(player.getCurrentTrackSelections(), C.TRACK_TYPE_VIDEO)) {
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
// onRenderedFirstFrame().
hideArtwork();
return;
}
// Video disabled so the shutter must be closed.

View File

@ -436,14 +436,10 @@ public class StyledPlayerControlView extends FrameLayout {
private StyledPlayerControlViewLayoutManager controlViewLayoutManager;
private Resources resources;
private int selectedMainSettingsPosition;
private RecyclerView settingsView;
private SettingsAdapter settingsAdapter;
private SubSettingsAdapter subSettingsAdapter;
private PlaybackSpeedAdapter playbackSpeedAdapter;
private PopupWindow settingsWindow;
private String[] playbackSpeedTexts;
private int[] playbackSpeedsMultBy100;
private int selectedPlaybackSpeedIndex;
private boolean needToHideBars;
private int settingsWindowMargin;
@ -457,6 +453,8 @@ public class StyledPlayerControlView extends FrameLayout {
@Nullable private ImageView fullScreenButton;
@Nullable private ImageView minimalFullScreenButton;
@Nullable private View settingsButton;
@Nullable private View playbackSpeedButton;
@Nullable private View audioTrackButton;
public StyledPlayerControlView(Context context) {
this(context, /* attrs= */ null);
@ -575,6 +573,16 @@ public class StyledPlayerControlView extends FrameLayout {
settingsButton.setOnClickListener(componentListener);
}
playbackSpeedButton = findViewById(R.id.exo_playback_speed);
if (playbackSpeedButton != null) {
playbackSpeedButton.setOnClickListener(componentListener);
}
audioTrackButton = findViewById(R.id.exo_audio_track);
if (audioTrackButton != null) {
audioTrackButton.setOnClickListener(componentListener);
}
TimeBar customTimeBar = findViewById(R.id.exo_progress);
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
if (customTimeBar != null) {
@ -663,12 +671,7 @@ public class StyledPlayerControlView extends FrameLayout {
settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
resources.getDrawable(R.drawable.exo_styled_controls_audiotrack);
settingsAdapter = new SettingsAdapter(settingTexts, settingIcons);
playbackSpeedTexts = resources.getStringArray(R.array.exo_playback_speeds);
playbackSpeedsMultBy100 = resources.getIntArray(R.array.exo_speed_multiplied_by_100);
settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
subSettingsAdapter = new SubSettingsAdapter();
settingsView =
(RecyclerView)
LayoutInflater.from(context).inflate(R.layout.exo_styled_settings_list, null);
@ -693,6 +696,10 @@ public class StyledPlayerControlView extends FrameLayout {
resources.getString(R.string.exo_controls_cc_disabled_description);
textTrackSelectionAdapter = new TextTrackSelectionAdapter();
audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
playbackSpeedAdapter =
new PlaybackSpeedAdapter(
resources.getStringArray(R.array.exo_playback_speeds),
resources.getIntArray(R.array.exo_speed_multiplied_by_100));
fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit);
fullScreenEnterDrawable =
@ -770,7 +777,6 @@ public class StyledPlayerControlView extends FrameLayout {
this.trackSelector = null;
}
updateAll();
updateSettingsPlaybackSpeedLists();
}
/**
@ -1102,6 +1108,7 @@ public class StyledPlayerControlView extends FrameLayout {
updateRepeatModeButton();
updateShuffleButton();
updateTrackLists();
updatePlaybackSpeedList();
updateTimeline();
}
@ -1141,11 +1148,12 @@ public class StyledPlayerControlView extends FrameLayout {
timeline.getWindow(player.getCurrentWindowIndex(), window);
boolean isSeekable = window.isSeekable;
enableSeeking = isSeekable;
enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();
enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
enableNext =
window.isLive() || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
(window.isLive() && window.isDynamic)
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
}
@ -1438,24 +1446,13 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
private void updateSettingsPlaybackSpeedLists() {
private void updatePlaybackSpeedList() {
if (player == null) {
return;
}
float speed = player.getPlaybackParameters().speed;
int currentSpeedMultBy100 = Math.round(speed * 100);
int closestMatchIndex = 0;
int closestMatchDifference = Integer.MAX_VALUE;
for (int i = 0; i < playbackSpeedsMultBy100.length; i++) {
int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]);
if (difference < closestMatchDifference) {
closestMatchIndex = i;
closestMatchDifference = difference;
}
}
selectedPlaybackSpeedIndex = closestMatchIndex;
playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed);
settingsAdapter.setSubTextAtPosition(
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedTexts[closestMatchIndex]);
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText());
}
private void updateSettingsWindowSize() {
@ -1571,27 +1568,14 @@ public class StyledPlayerControlView extends FrameLayout {
private void onSettingViewClicked(int position) {
if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
subSettingsAdapter.init(playbackSpeedTexts, selectedPlaybackSpeedIndex);
selectedMainSettingsPosition = SETTINGS_PLAYBACK_SPEED_POSITION;
displaySettingsWindow(subSettingsAdapter);
displaySettingsWindow(playbackSpeedAdapter);
} else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
selectedMainSettingsPosition = SETTINGS_AUDIO_TRACK_SELECTION_POSITION;
displaySettingsWindow(audioTrackSelectionAdapter);
} else {
settingsWindow.dismiss();
}
}
private void onSubSettingViewClicked(int position) {
if (selectedMainSettingsPosition == SETTINGS_PLAYBACK_SPEED_POSITION) {
if (position != selectedPlaybackSpeedIndex) {
float speed = playbackSpeedsMultBy100[position] / 100.0f;
setPlaybackSpeed(speed);
}
}
settingsWindow.dismiss();
}
private void onLayoutChange(
View v,
int left,
@ -1836,7 +1820,7 @@ public class StyledPlayerControlView extends FrameLayout {
updateTimeline();
}
if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
updateSettingsPlaybackSpeedLists();
updatePlaybackSpeedList();
}
if (events.contains(EVENT_TRACKS_CHANGED)) {
updateTrackLists();
@ -1877,6 +1861,12 @@ public class StyledPlayerControlView extends FrameLayout {
} else if (settingsButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(settingsAdapter);
} else if (playbackSpeedButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(playbackSpeedAdapter);
} else if (audioTrackButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(audioTrackSelectionAdapter);
} else if (subtitleButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(textTrackSelectionAdapter);
@ -1950,18 +1940,33 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
private class SubSettingsAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
private final class PlaybackSpeedAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
private String[] texts;
private final String[] playbackSpeedTexts;
private final int[] playbackSpeedsMultBy100;
private int selectedIndex;
public SubSettingsAdapter() {
texts = new String[0];
public PlaybackSpeedAdapter(String[] playbackSpeedTexts, int[] playbackSpeedsMultBy100) {
this.playbackSpeedTexts = playbackSpeedTexts;
this.playbackSpeedsMultBy100 = playbackSpeedsMultBy100;
}
public void init(String[] texts, int selectedIndex) {
this.texts = texts;
this.selectedIndex = selectedIndex;
public void updateSelectedIndex(float playbackSpeed) {
int currentSpeedMultBy100 = Math.round(playbackSpeed * 100);
int closestMatchIndex = 0;
int closestMatchDifference = Integer.MAX_VALUE;
for (int i = 0; i < playbackSpeedsMultBy100.length; i++) {
int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]);
if (difference < closestMatchDifference) {
closestMatchIndex = i;
closestMatchDifference = difference;
}
}
selectedIndex = closestMatchIndex;
}
public String getSelectedText() {
return playbackSpeedTexts[selectedIndex];
}
@Override
@ -1974,27 +1979,23 @@ public class StyledPlayerControlView extends FrameLayout {
@Override
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
if (position < texts.length) {
holder.textView.setText(texts[position]);
if (position < playbackSpeedTexts.length) {
holder.textView.setText(playbackSpeedTexts[position]);
}
holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
holder.itemView.setOnClickListener(
v -> {
if (position != selectedIndex) {
float speed = playbackSpeedsMultBy100[position] / 100.0f;
setPlaybackSpeed(speed);
}
settingsWindow.dismiss();
});
}
@Override
public int getItemCount() {
return texts.length;
}
}
private final class SubSettingViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
private final View checkView;
public SubSettingViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.exo_text);
checkView = itemView.findViewById(R.id.exo_check);
itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition()));
return playbackSpeedTexts.length;
}
}
@ -2042,7 +2043,7 @@ public class StyledPlayerControlView extends FrameLayout {
}
@Override
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
// CC options include "Off" at the first position, which disables text rendering.
holder.textView.setText(R.string.exo_track_selection_none);
boolean isTrackSelectionOff = true;
@ -2071,7 +2072,7 @@ public class StyledPlayerControlView extends FrameLayout {
}
@Override
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
if (position > 0) {
TrackInfo track = tracks.get(position - 1);
@ -2088,7 +2089,7 @@ public class StyledPlayerControlView extends FrameLayout {
private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
@Override
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
// Audio track selection option includes "Auto" at the top.
holder.textView.setText(R.string.exo_track_selection_auto);
// hasSelectionOverride is true means there is an explicit track selection, not "Auto".
@ -2167,8 +2168,7 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
private abstract class TrackSelectionAdapter
extends RecyclerView.Adapter<TrackSelectionViewHolder> {
private abstract class TrackSelectionAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
protected List<Integer> rendererIndices;
protected List<TrackInfo> tracks;
@ -2184,19 +2184,19 @@ public class StyledPlayerControlView extends FrameLayout {
List<Integer> rendererIndices, List<TrackInfo> trackInfos, MappedTrackInfo mappedTrackInfo);
@Override
public TrackSelectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v =
LayoutInflater.from(getContext())
.inflate(R.layout.exo_styled_sub_settings_list_item, null);
return new TrackSelectionViewHolder(v);
return new SubSettingViewHolder(v);
}
public abstract void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder);
public abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder);
public abstract void onTrackSelection(String subtext);
@Override
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
if (trackSelector == null || mappedTrackInfo == null) {
return;
}
@ -2252,12 +2252,12 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
private static class TrackSelectionViewHolder extends RecyclerView.ViewHolder {
private static class SubSettingViewHolder extends RecyclerView.ViewHolder {
public final TextView textView;
public final View checkView;
public TrackSelectionViewHolder(View itemView) {
public SubSettingViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.exo_text);
checkView = itemView.findViewById(R.id.exo_check);

View File

@ -607,7 +607,7 @@ import java.util.List;
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true);
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false);
} else if (uxState != UX_STATE_ANIMATING_HIDE && uxState != UX_STATE_ANIMATING_SHOW) {
} else if (uxState != UX_STATE_ANIMATING_HIDE) {
defaultTimeBar.showScrubber();
}
}

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