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 .DS_Store
cmake-build-debug cmake-build-debug
dist dist
jacoco.exec
tmp tmp
# External native builds # External native builds

View File

@ -2,22 +2,29 @@
### dev-v2 (not yet released) ### dev-v2 (not yet released)
* Extractors:
* Add support for MP4 and QuickTime meta atoms that are not full atoms.
* UI: * UI:
* Add builder for `PlayerNotificationManager`. * Add builder for `PlayerNotificationManager`.
* Add group setting to `PlayerNotificationManager`.
* Audio: * Audio:
* Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases
([#8585](https://github.com/google/ExoPlayer/issues/8585)).
* Report unexpected discontinuities in * Report unexpected discontinuities in
`AnalyticsListener.onAudioSinkError` `AnalyticsListener.onAudioSinkError`
([#6384](https://github.com/google/ExoPlayer/issues/6384)). ([#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: * Analytics:
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`. * 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: * Library restructuring:
* `DebugTextViewHelper` moved from `ui` package to `util` package. * `DebugTextViewHelper` moved from `ui` package to `util` package.
* Spherical UI components moved from `video.spherical` package to * Spherical UI components moved from `video.spherical` package to
`ui.spherical` package, and made package private. `ui.spherical` package, and made package private.
* Core
* Move `getRendererCount` and `getRendererType` methods from `Player` to
`ExoPlayer`.
* Remove deprecated symbols: * Remove deprecated symbols:
* Remove `Player.DefaultEventListener`. Use `Player.EventListener` * Remove `Player.DefaultEventListener`. Use `Player.EventListener`
instead. instead.
@ -25,6 +32,33 @@
instead. instead.
* Remove `extension-jobdispatcher` module. Use the `extension-workmanager` * Remove `extension-jobdispatcher` module. Use the `extension-workmanager`
module instead. 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: * IMA extension:
* Fix a bug where playback could get stuck when seeking into a playlist * 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 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 * 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 index was used when deciding whether to trigger playback of an ad after
a seek. 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)). ([#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)). ([#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)). ([#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)). ([#8581](https://github.com/google/ExoPlayer/issues/8581)).
### 2.13.1 (2021-02-12) ### 2.13.1 (2021-02-12)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -484,26 +484,6 @@ public final class CastPlayer extends BasePlayer {
sessionManager.endCurrentSession(false); 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 @Override
public void setRepeatMode(@RepeatMode int repeatMode) { public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
@ -708,15 +688,19 @@ public final class CastPlayer extends BasePlayer {
} }
} }
@SuppressWarnings("deprecation") // Calling deprecated listener method.
private void updateTimelineAndNotifyIfChanged() { private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) { if (updateTimeline()) {
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
Timeline timeline = currentTimeline;
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED, Player.EVENT_TIMELINE_CHANGED,
listener -> listener -> {
listener.onTimelineChanged( listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); 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. // Accessed by the calling thread only.
private boolean opened; private boolean opened;
private long bytesToSkip;
private long bytesRemaining; private long bytesRemaining;
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible // 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; byte[] responseBody;
try { try {
responseBody = readResponseBody(); responseBody = readResponseBody();
} catch (HttpDataSourceException e) { } catch (IOException e) {
responseBody = Util.EMPTY_BYTE_ARRAY; 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 // 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 // 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position. // 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. // Calculate the content length.
if (!isCompressed(responseInfo)) { if (!isCompressed(responseInfo)) {
@ -627,6 +626,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
opened = true; opened = true;
transferStarted(dataSpec); 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; return bytesRemaining;
} }
@ -641,25 +648,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} }
ByteBuffer readBuffer = getOrCreateReadBuffer(); ByteBuffer readBuffer = getOrCreateReadBuffer();
while (!readBuffer.hasRemaining()) { if (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet. // Fill readBuffer with more data from Cronet.
operation.close(); operation.close();
readBuffer.clear(); readBuffer.clear();
readInternal(readBuffer); try {
readInternal(readBuffer);
} catch (IOException e) {
throw new HttpDataSourceException(
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
}
if (finished) { if (finished) {
bytesRemaining = 0; bytesRemaining = 0;
return C.RESULT_END_OF_INPUT; 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 // 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(); int readLength = buffer.remaining();
if (readBuffer != null) { 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. // If there is existing data in the readBuffer, read as much as possible. Return if any read.
int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer); int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
if (copyBytes != 0) { if (copyBytes != 0) {
@ -740,44 +736,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} }
} }
boolean readMore = true; // Fill buffer with more data from Cronet.
while (readMore) { operation.close();
// If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's try {
// buffer. If we do not need to skip bytes, we may write to buffer directly. readInternal(buffer);
final boolean useCallerBuffer = bytesToSkip == 0; } catch (IOException e) {
throw new HttpDataSourceException(
operation.close(); e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
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();
}
}
} }
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) { if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead; bytesRemaining -= bytesRead;
} }
@ -885,13 +860,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; 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. * Reads the whole response body.
* *
* @return The 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; byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
ByteBuffer readBuffer = getOrCreateReadBuffer(); ByteBuffer readBuffer = getOrCreateReadBuffer();
while (!finished) { 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. * 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. * @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") @SuppressWarnings("ReferenceEquality")
private void readInternal(ByteBuffer buffer) throws HttpDataSourceException { private void readInternal(ByteBuffer buffer) throws IOException {
castNonNull(currentUrlRequest).read(buffer); castNonNull(currentUrlRequest).read(buffer);
try { try {
if (!operation.block(readTimeoutMs)) { if (!operation.block(readTimeoutMs)) {
@ -930,23 +941,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
readBuffer = null; readBuffer = null;
} }
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new HttpDataSourceException( throw new InterruptedIOException();
new InterruptedIOException(),
castNonNull(currentDataSpec),
HttpDataSourceException.TYPE_READ);
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
// The operation is ongoing so replace buffer to avoid it being written to by this // The operation is ongoing so replace buffer to avoid it being written to by this
// operation during a subsequent request. // operation during a subsequent request.
if (buffer == readBuffer) { if (buffer == readBuffer) {
readBuffer = null; readBuffer = null;
} }
throw new HttpDataSourceException( throw e;
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
} }
if (exception != null) { if (exception != null) {
throw new HttpDataSourceException( throw exception;
exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
} }
} }

View File

@ -256,6 +256,7 @@ public final class CronetDataSourceTest {
public void requestSetsRangeHeader() throws HttpDataSourceException { public void requestSetsRangeHeader() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
mockResponseStartSuccess(); mockResponseStartSuccess();
mockReadSuccess(0, 1000);
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1. // The header value to add is current position to current position + length - 1.
@ -287,8 +288,6 @@ public final class CronetDataSourceTest {
testDataSpec = testDataSpec =
new DataSpec.Builder() new DataSpec.Builder()
.setUri(TEST_URL) .setUri(TEST_URL)
.setPosition(1000)
.setLength(5000)
.setHttpRequestHeaders(dataSpecRequestProperties) .setHttpRequestHeaders(dataSpecRequestProperties)
.build(); .build();
mockResponseStartSuccess(); mockResponseStartSuccess();
@ -1198,6 +1197,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
mockSingleRedirectSuccess(); mockSingleRedirectSuccess();
mockReadSuccess(0, 1000);
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video"); testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
@ -1368,7 +1368,7 @@ public final class CronetDataSourceTest {
@Test @Test
public void allowDirectExecutor() throws HttpDataSourceException { public void allowDirectExecutor() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000); testDataSpec = new DataSpec(Uri.parse(TEST_URL));
mockResponseStartSuccess(); mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec); 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. * 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>" 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. * 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>" 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.AdsLoader.OverlayInfo;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.upstream.DataSpec;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -700,12 +701,7 @@ import java.util.Map;
// Check for a selected track using an audio renderer. // Check for a selected track using an audio renderer.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections(); TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) { return TrackSelectionUtil.hasTrackOfType(trackSelections, C.TRACK_TYPE_AUDIO) ? 100 : 0;
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
return 100;
}
}
return 0;
} }
private void handleAdEvent(AdEvent adEvent) { private void handleAdEvent(AdEvent adEvent) {

View File

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

View File

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

View File

@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-common')
api 'androidx.media:media:' + androidxMediaVersion api 'androidx.media:media:' + androidxMediaVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion 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 androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player; 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 com.google.android.exoplayer2.util.Util;
import java.util.List; import java.util.List;
/** /**
* A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link * A {@link MediaSessionConnector.QueueEditor} implementation.
* ConcatenatingMediaSource}.
* *
* <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles * <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. * 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_FROM_INDEX = "from_index";
public static final String EXTRA_TO_INDEX = "to_index"; public static final String EXTRA_TO_INDEX = "to_index";
/** /** Converts a {@link MediaDescriptionCompat} to a {@link MediaItem}. */
* Factory to create {@link MediaSource}s. public interface MediaDescriptionConverter {
*/
public interface MediaSourceFactory {
/** /**
* 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. * <p>If not null, the media item that is returned will be used to call {@link
* @return A {@link MediaSource} or {@code null} if no source can be created for the given * Player#addMediaItem(MediaItem)}.
* description.
*/ */
@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) { public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {
return Util.areEqual(d1.getMediaId(), d2.getMediaId()); return Util.areEqual(d1.getMediaId(), d2.getMediaId());
} }
} }
private final MediaControllerCompat mediaController; private final MediaControllerCompat mediaController;
private final QueueDataAdapter queueDataAdapter; private final QueueDataAdapter queueDataAdapter;
private final MediaSourceFactory sourceFactory; private final MediaDescriptionConverter mediaDescriptionConverter;
private final MediaDescriptionEqualityChecker equalityChecker; private final MediaDescriptionEqualityChecker equalityChecker;
private final ConcatenatingMediaSource queueMediaSource;
/** /**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
* *
* @param mediaController A {@link MediaControllerCompat} to read the current queue. * @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 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( public TimelineQueueEditor(
MediaControllerCompat mediaController, MediaControllerCompat mediaController,
ConcatenatingMediaSource queueMediaSource,
QueueDataAdapter queueDataAdapter, QueueDataAdapter queueDataAdapter,
MediaSourceFactory sourceFactory) { MediaDescriptionConverter mediaDescriptionConverter) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, this(
new MediaIdEqualityChecker()); mediaController, queueDataAdapter, mediaDescriptionConverter, new MediaIdEqualityChecker());
} }
/** /**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
* *
* @param mediaController A {@link MediaControllerCompat} to read the current queue. * @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 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. * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/ */
public TimelineQueueEditor( public TimelineQueueEditor(
MediaControllerCompat mediaController, MediaControllerCompat mediaController,
ConcatenatingMediaSource queueMediaSource,
QueueDataAdapter queueDataAdapter, QueueDataAdapter queueDataAdapter,
MediaSourceFactory sourceFactory, MediaDescriptionConverter mediaDescriptionConverter,
MediaDescriptionEqualityChecker equalityChecker) { MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController; this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource;
this.queueDataAdapter = queueDataAdapter; this.queueDataAdapter = queueDataAdapter;
this.sourceFactory = sourceFactory; this.mediaDescriptionConverter = mediaDescriptionConverter;
this.equalityChecker = equalityChecker; this.equalityChecker = equalityChecker;
} }
@ -165,10 +157,10 @@ public final class TimelineQueueEditor
@Override @Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) { public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
@Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description); @Nullable MediaItem mediaItem = mediaDescriptionConverter.convert(description);
if (mediaSource != null) { if (mediaItem != null) {
queueDataAdapter.add(index, description); 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++) { for (int i = 0; i < queue.size(); i++) {
if (equalityChecker.equals(queue.get(i).getDescription(), description)) { if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
queueDataAdapter.remove(i); queueDataAdapter.remove(i);
queueMediaSource.removeMediaSource(i); player.removeMediaItem(i);
return; return;
} }
} }
@ -200,9 +192,8 @@ public final class TimelineQueueEditor
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET); int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) { if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
queueDataAdapter.move(from, to); queueDataAdapter.move(from, to);
queueMediaSource.moveMediaSource(from, to); player.moveMediaItem(from, to);
} }
return true; return true;
} }
} }

View File

@ -98,8 +98,10 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
if (!timeline.isEmpty() && !player.isPlayingAd()) { if (!timeline.isEmpty() && !player.isPlayingAd()) {
timeline.getWindow(player.getCurrentWindowIndex(), window); timeline.getWindow(player.getCurrentWindowIndex(), window);
enableSkipTo = timeline.getWindowCount() > 1; enableSkipTo = timeline.getWindowCount() > 1;
enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); enablePrevious = window.isSeekable || !window.isLive() || player.hasPrevious();
enableNext = window.isDynamic || player.hasNext(); enableNext =
(window.isLive() && window.isDynamic)
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
} }
long actions = 0; 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 Call.Factory callFactory;
private final RequestProperties requestProperties; private final RequestProperties requestProperties;
@ -183,10 +181,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
@Nullable private InputStream responseByteStream; @Nullable private InputStream responseByteStream;
private boolean opened; private boolean opened;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped; private long bytesSkipped;
private long bytesToRead;
private long bytesRead; private long bytesRead;
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */ /** @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 // 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 // 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position. // 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. // Determine the length of the data to be read, after skipping.
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
@ -345,13 +341,21 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
opened = true; opened = true;
transferStarted(dataSpec); 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; return bytesToRead;
} }
@Override @Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try { try {
skipInternal();
return readInternal(buffer, offset, readLength); return readInternal(buffer, offset, readLength);
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException( 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 * Returns the number of bytes that were skipped during the most recent call to {@link
* {@link #open(DataSpec)}. * #open(DataSpec)}.
* *
* @return The number of bytes skipped. * @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. * Attempts to skip the specified number of bytes in full.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
* *
* @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation. * @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 { private boolean skipFully(long bytesToSkip) throws IOException {
if (bytesSkipped == bytesToSkip) { if (bytesToSkip == 0) {
return; return true;
} }
byte[] skipBuffer = new byte[4096];
while (bytesSkipped != bytesToSkip) { while (bytesSkipped != bytesToSkip) {
int readLength = (int) min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length); int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength); int read = castNonNull(responseByteStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) { if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException(); throw new InterruptedIOException();
} }
if (read == -1) { if (read == -1) {
throw new EOFException(); return false;
} }
bytesSkipped += read; bytesSkipped += read;
bytesTransferred(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. * 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>" 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. * 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>" NDK_PATH="<path to Android NDK>"

View File

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

View File

@ -79,11 +79,12 @@ public class DefaultControlDispatcher implements ControlDispatcher {
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window); timeline.getWindow(windowIndex, window);
int previousWindowIndex = player.getPreviousWindowIndex(); int previousWindowIndex = player.getPreviousWindowIndex();
boolean isUnseekableLiveStream = window.isLive() && !window.isSeekable;
if (previousWindowIndex != C.INDEX_UNSET if (previousWindowIndex != C.INDEX_UNSET
&& (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (window.isDynamic && !window.isSeekable))) { || isUnseekableLiveStream)) {
player.seekTo(previousWindowIndex, C.TIME_UNSET); player.seekTo(previousWindowIndex, C.TIME_UNSET);
} else { } else if (!isUnseekableLiveStream) {
player.seekTo(windowIndex, /* positionMs= */ 0); player.seekTo(windowIndex, /* positionMs= */ 0);
} }
return true; return true;
@ -96,10 +97,11 @@ public class DefaultControlDispatcher implements ControlDispatcher {
return true; return true;
} }
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
int nextWindowIndex = player.getNextWindowIndex(); int nextWindowIndex = player.getNextWindowIndex();
if (nextWindowIndex != C.INDEX_UNSET) { if (nextWindowIndex != C.INDEX_UNSET) {
player.seekTo(nextWindowIndex, C.TIME_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); player.seekTo(windowIndex, C.TIME_UNSET);
} }
return true; 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". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.13.1"; public static final String VERSION = "2.13.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.1"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2";
/** /**
* The version of the library expressed as an integer, for example 1002003. * 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). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2013001; public static final int VERSION_INT = 2013002;
/** /**
* The default user agent for requests made by the library. * 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.offline.StreamKey;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
@ -937,6 +938,7 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
FIELD_TARGET_OFFSET_MS, FIELD_TARGET_OFFSET_MS,
@ -1148,6 +1150,7 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
FIELD_START_POSITION_MS, FIELD_START_POSITION_MS,
@ -1254,6 +1257,7 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
FIELD_MEDIA_ID, FIELD_MEDIA_ID,

View File

@ -19,6 +19,7 @@ import android.os.Bundle;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -69,10 +70,9 @@ public final class MediaMetadata implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({FIELD_TITLE})
FIELD_TITLE,
})
private @interface FieldNumber {} private @interface FieldNumber {}
private static final int FIELD_TITLE = 0; 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()}. * which can be obtained by calling {@link #getCurrentTimeline()}.
* <li>They can provide a {@link TrackGroupArray} defining the currently available tracks, which * <li>They can provide a {@link TrackGroupArray} defining the currently available tracks, which
* can be obtained by calling {@link #getCurrentTrackGroups()}. * 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 * <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()}}. * #getCurrentTrackSelections()}}.
* </ul> * </ul>
*/ */
@ -130,13 +127,17 @@ public interface Player {
void clearAuxEffectInfo(); 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); 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(); float getVolume();
/** /**
@ -400,30 +401,9 @@ public interface Player {
* @param timeline The latest timeline. Never null, but may be empty. * @param timeline The latest timeline. Never null, but may be empty.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/ */
@SuppressWarnings("deprecation") default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {}
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);
}
/** /**
* 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 * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
* window).manifest} for a given window index. * 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. * 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 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 * @param trackSelections The selected tracks. Never null, but may contain null elements. A
* length {@link #getRendererCount()}, but may contain null elements. * 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( default void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
@ -488,10 +470,7 @@ public interface Player {
* *
* @param isLoading Whether the source is currently being loaded. * @param isLoading Whether the source is currently being loaded.
*/ */
@SuppressWarnings("deprecation") default void onIsLoadingChanged(boolean isLoading) {}
default void onIsLoadingChanged(boolean isLoading) {
onLoadingChanged(isLoading);
}
/** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */ /** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */
@Deprecated @Deprecated
@ -1131,6 +1110,7 @@ public interface Player {
* Returns the current {@link State playback state} of the player. * Returns the current {@link State playback state} of the player.
* *
* @return The current {@link State playback state}. * @return The current {@link State playback state}.
* @see EventListener#onPlaybackStateChanged(int)
*/ */
@State @State
int getPlaybackState(); int getPlaybackState();
@ -1140,6 +1120,7 @@ public interface Player {
* true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed. * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
* *
* @return The current {@link PlaybackSuppressionReason playback suppression reason}. * @return The current {@link PlaybackSuppressionReason playback suppression reason}.
* @see EventListener#onPlaybackSuppressionReasonChanged(int)
*/ */
@PlaybackSuppressionReason @PlaybackSuppressionReason
int getPlaybackSuppressionReason(); int getPlaybackSuppressionReason();
@ -1156,6 +1137,7 @@ public interface Player {
* </ul> * </ul>
* *
* @return Whether the player is playing. * @return Whether the player is playing.
* @see EventListener#onIsPlayingChanged(boolean)
*/ */
boolean isPlaying(); boolean isPlaying();
@ -1168,6 +1150,7 @@ public interface Player {
* {@link #STATE_IDLE}. * {@link #STATE_IDLE}.
* *
* @return The error, or {@code null}. * @return The error, or {@code null}.
* @see EventListener#onPlayerError(ExoPlaybackException)
*/ */
@Nullable @Nullable
ExoPlaybackException getPlayerError(); ExoPlaybackException getPlayerError();
@ -1199,6 +1182,7 @@ public interface Player {
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
* *
* @return Whether playback will proceed when ready. * @return Whether playback will proceed when ready.
* @see EventListener#onPlayWhenReadyChanged(boolean, int)
*/ */
boolean getPlayWhenReady(); boolean getPlayWhenReady();
@ -1213,6 +1197,7 @@ public interface Player {
* Returns the current {@link RepeatMode} used for playback. * Returns the current {@link RepeatMode} used for playback.
* *
* @return The current repeat mode. * @return The current repeat mode.
* @see EventListener#onRepeatModeChanged(int)
*/ */
@RepeatMode @RepeatMode
int getRepeatMode(); int getRepeatMode();
@ -1224,13 +1209,18 @@ public interface Player {
*/ */
void setShuffleModeEnabled(boolean shuffleModeEnabled); void setShuffleModeEnabled(boolean shuffleModeEnabled);
/** Returns whether shuffling of windows is enabled. */ /**
* Returns whether shuffling of windows is enabled.
*
* @see EventListener#onShuffleModeEnabledChanged(boolean)
*/
boolean getShuffleModeEnabled(); boolean getShuffleModeEnabled();
/** /**
* Whether the player is currently loading the source. * Whether the player is currently loading the source.
* *
* @return Whether the player is currently loading the source. * @return Whether the player is currently loading the source.
* @see EventListener#onIsLoadingChanged(boolean)
*/ */
boolean isLoading(); boolean isLoading();
@ -1325,6 +1315,18 @@ public interface Player {
*/ */
void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); 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. * Returns the currently active playback parameters.
* *
@ -1359,24 +1361,22 @@ public interface Player {
*/ */
void release(); 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 * @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
* 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 available track groups. */
TrackGroupArray getCurrentTrackGroups(); 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(); TrackSelectionArray getCurrentTrackSelections();
/** /**
@ -1389,6 +1389,8 @@ public interface Player {
* *
* <p>This metadata is considered static in that it comes from the tracks' declared Formats, * <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. * rather than being timed (or dynamic) metadata, which is represented within a metadata track.
*
* @see EventListener#onStaticMetadataChanged(List)
*/ */
List<Metadata> getCurrentStaticMetadata(); List<Metadata> getCurrentStaticMetadata();
@ -1398,7 +1400,11 @@ public interface Player {
@Nullable @Nullable
Object getCurrentManifest(); 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(); Timeline getCurrentTimeline();
/** Returns the index of the period currently being played. */ /** 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 * Returns the media item of the current window in the timeline. May be null if the timeline is
* empty. * empty.
*
* @see EventListener#onMediaItemTransition(MediaItem, int)
*/ */
@Nullable @Nullable
MediaItem getCurrentMediaItem(); MediaItem getCurrentMediaItem();

View File

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

View File

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

View File

@ -41,6 +41,10 @@ public final class DataSourceException extends IOException {
return false; 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; public static final int POSITION_OUT_OF_RANGE = 0;
/** /**
@ -56,5 +60,4 @@ public final class DataSourceException extends IOException {
public DataSourceException(int reason) { public DataSourceException(int reason) {
this.reason = reason; this.reason = reason;
} }
} }

View File

@ -46,7 +46,6 @@ import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. * 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 DataSpec dataSpec;
@Nullable private HttpURLConnection connection; @Nullable private HttpURLConnection connection;
@Nullable private InputStream inputStream; @Nullable private InputStream inputStream;
private byte @MonotonicNonNull [] skipBuffer;
private boolean opened; private boolean opened;
private int responseCode; private int responseCode;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped; private long bytesSkipped;
private long bytesToRead;
private long bytesRead; private long bytesRead;
/** @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */ /** @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 // 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 // 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position. // 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. // Determine the length of the data to be read, after skipping.
boolean isCompressed = isCompressed(connection); boolean isCompressed = isCompressed(connection);
@ -432,13 +428,21 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
opened = true; opened = true;
transferStarted(dataSpec); 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; return bytesToRead;
} }
@Override @Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try { try {
skipInternal();
return readInternal(buffer, offset, readLength); return readInternal(buffer, offset, readLength);
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException( 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 * Returns the number of bytes that were skipped during the most recent call to {@link
* {@link #open(DataSpec)}. * #open(DataSpec)}.
* *
* @return The number of bytes skipped. * @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. * Attempts to skip the specified number of bytes in full.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
* *
* @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation. * @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 { private boolean skipFully(long bytesToSkip) throws IOException {
if (bytesSkipped == bytesToSkip) { if (bytesToSkip == 0) {
return; return true;
} }
byte[] skipBuffer = new byte[4096];
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
while (bytesSkipped != bytesToSkip) { while (bytesSkipped != bytesToSkip) {
int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length); int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = castNonNull(inputStream).read(skipBuffer, 0, readLength); int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
@ -748,11 +749,12 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
throw new InterruptedIOException(); throw new InterruptedIOException();
} }
if (read == -1) { if (read == -1) {
throw new EOFException(); return false;
} }
bytesSkipped += read; bytesSkipped += read;
bytesTransferred(read); bytesTransferred(read);
} }
return true;
} }
/** /**

View File

@ -138,4 +138,11 @@ public final class CopyOnWriteMultiset<E extends Object> implements Iterable<E>
return elements.iterator(); 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")); 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 com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail; import static junit.framework.Assert.fail;
import static org.junit.Assert.assertThrows;
import android.net.Uri; import android.net.Uri;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.ContentDataSource.ContentDataSourceException;
import java.io.EOFException;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; 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 { private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
ContentDataSource dataSource = ContentDataSource dataSource =
@ -130,5 +97,4 @@ public final class ContentDataSourceTest {
dataSource.close(); 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.Bundle;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -73,7 +72,7 @@ public final class TestContentProvider extends ContentProvider
openPipeHelper( openPipeHelper(
uri, /* mimeType= */ null, /* opts= */ null, /* args= */ null, /* func= */ this); uri, /* mimeType= */ null, /* opts= */ null, /* args= */ null, /* func= */ this);
return new AssetFileDescriptor( return new AssetFileDescriptor(
fileDescriptor, /* startOffset= */ 0, /* length= */ C.LENGTH_UNSET); fileDescriptor, /* startOffset= */ 0, AssetFileDescriptor.UNKNOWN_LENGTH);
} else { } else {
return getContext().getAssets().openFd(fileName); 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(), new DefaultAudioProcessorChain(),
enableFloatOutput, enableFloatOutput,
enableAudioTrackPlaybackParams, 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}, * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
* Renderer consumes media from the MediaSource being played. Renderers are injected when the * 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 * <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 * consumed by each of the available Renderers. The library provides a default implementation
* ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected * ({@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. * 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 * <li>Audio offload rendering is enabled in {@link
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link * DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
* DefaultAudioSink#DefaultAudioSink(AudioCapabilities, * 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, * <li>An audio track is playing in a format that the device supports offloading (for example,
* MP3 or AAC). * MP3 or AAC).
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}. * <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. * Returns whether the player has paused its main loop to save power in offload scheduling mode.
* *
* @see #experimentalSetOffloadSchedulingEnabled(boolean) * @see #experimentalSetOffloadSchedulingEnabled(boolean)
* @see EventListener#onExperimentalSleepingForOffloadChanged(boolean)
*/ */
boolean experimentalIsSleepingForOffload(); boolean experimentalIsSleepingForOffload();
} }

View File

@ -999,7 +999,16 @@ import java.util.List;
if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) { if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) {
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED, 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) { if (positionDiscontinuity) {
listeners.queueEvent( listeners.queueEvent(
@ -1042,7 +1051,10 @@ import java.util.List;
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) { if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_IS_LOADING_CHANGED, Player.EVENT_IS_LOADING_CHANGED,
listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading)); listener -> {
listener.onLoadingChanged(newPlaybackInfo.isLoading);
listener.onIsLoadingChanged(newPlaybackInfo.isLoading);
});
} }
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState
|| previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) { || previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {

View File

@ -1980,7 +1980,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod(); @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod();
if (readingPeriod == null if (readingPeriod == null
|| queue.getPlayingPeriod() == readingPeriod || queue.getPlayingPeriod() == readingPeriod
|| readingPeriod.allRenderersEnabled) { || readingPeriod.allRenderersInCorrectState) {
// Not reading ahead or all renderers updated. // Not reading ahead or all renderers updated.
return; return;
} }
@ -2075,7 +2075,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();
return nextPlayingPeriodHolder != null return nextPlayingPeriodHolder != null
&& rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime() && rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime()
&& nextPlayingPeriodHolder.allRenderersEnabled; && nextPlayingPeriodHolder.allRenderersInCorrectState;
} }
private boolean hasReadingPeriodFinishedReading() { private boolean hasReadingPeriodFinishedReading() {
@ -2294,7 +2294,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
enableRenderer(i, rendererWasEnabledFlags[i]); enableRenderer(i, rendererWasEnabledFlags[i]);
} }
} }
readingMediaPeriod.allRenderersEnabled = true; readingMediaPeriod.allRenderersInCorrectState = true;
} }
private void enableRenderer(int rendererIndex, boolean wasRendererEnabled) 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. */ /** {@link MediaPeriodInfo} about this media period. */
public MediaPeriodInfo info; 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[], * {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[],
* SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[], * SampleStream, long, boolean, boolean, long, long)} or {@link Renderer#replaceStream(Format[],
* SampleStream, long)} has been called. * 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 boolean[] mayRetainStreamFlags;
private final RendererCapabilities[] rendererCapabilities; private final RendererCapabilities[] rendererCapabilities;

View File

@ -21,6 +21,7 @@ import static java.lang.Math.min;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.analytics.AnalyticsCollector; 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.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MaskingMediaPeriod; import com.google.android.exoplayer2.source.MaskingMediaPeriod;
@ -600,9 +601,11 @@ import java.util.Set;
@Override @Override
public void onDrmSessionAcquired( public void onDrmSessionAcquired(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
@DrmSession.State int state) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { 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. * 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}. * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
*/ */
int getTrackType(); int getTrackType();

View File

@ -1293,13 +1293,6 @@ public class SimpleExoPlayer extends BasePlayer
prepare(); prepare();
} }
@Override
public void setMediaItems(List<MediaItem> mediaItems) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItems(mediaItems);
}
@Override @Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) { public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
verifyApplicationThread(); verifyApplicationThread();
@ -1315,27 +1308,6 @@ public class SimpleExoPlayer extends BasePlayer
player.setMediaItems(mediaItems, startWindowIndex, startPositionMs); 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 @Override
public void setMediaSources(List<MediaSource> mediaSources) { public void setMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread(); verifyApplicationThread();
@ -1391,18 +1363,6 @@ public class SimpleExoPlayer extends BasePlayer
player.addMediaItems(index, mediaItems); 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 @Override
public void addMediaSource(MediaSource mediaSource) { public void addMediaSource(MediaSource mediaSource) {
verifyApplicationThread(); verifyApplicationThread();
@ -1427,24 +1387,12 @@ public class SimpleExoPlayer extends BasePlayer
player.addMediaSources(index, mediaSources); player.addMediaSources(index, mediaSources);
} }
@Override
public void moveMediaItem(int currentIndex, int newIndex) {
verifyApplicationThread();
player.moveMediaItem(currentIndex, newIndex);
}
@Override @Override
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
verifyApplicationThread(); verifyApplicationThread();
player.moveMediaItems(fromIndex, toIndex, newIndex); player.moveMediaItems(fromIndex, toIndex, newIndex);
} }
@Override
public void removeMediaItem(int index) {
verifyApplicationThread();
player.removeMediaItem(index);
}
@Override @Override
public void removeMediaItems(int fromIndex, int toIndex) { public void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThread(); verifyApplicationThread();
@ -2072,8 +2020,8 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public void onRenderedFirstFrame(Surface surface) { public void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {
analyticsCollector.onRenderedFirstFrame(surface); analyticsCollector.onRenderedFirstFrame(surface, renderTimeMs);
if (SimpleExoPlayer.this.surface == surface) { if (SimpleExoPlayer.this.surface == surface) {
for (VideoListener videoListener : videoListeners) { for (VideoListener videoListener : videoListeners) {
videoListener.onRenderedFirstFrame(); 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.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; 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.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
@ -207,7 +208,7 @@ public class AnalyticsCollector
// AudioRendererEventListener implementation. // AudioRendererEventListener implementation.
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onAudioEnabled(DecoderCounters counters) { public final void onAudioEnabled(DecoderCounters counters) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -220,7 +221,7 @@ public class AnalyticsCollector
}); });
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onAudioDecoderInitialized( public final void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) { String decoderName, long initializedTimestampMs, long initializationDurationMs) {
@ -230,12 +231,14 @@ public class AnalyticsCollector
AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED, AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED,
listener -> { listener -> {
listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs); listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs);
listener.onAudioDecoderInitialized(
eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
listener.onDecoderInitialized( listener.onDecoderInitialized(
eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
}); });
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onAudioInputFormatChanged( public final void onAudioInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
@ -244,6 +247,7 @@ public class AnalyticsCollector
eventTime, eventTime,
AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED, AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED,
listener -> { listener -> {
listener.onAudioInputFormatChanged(eventTime, format);
listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation);
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
}); });
@ -278,7 +282,7 @@ public class AnalyticsCollector
listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); listener -> listener.onAudioDecoderReleased(eventTime, decoderName));
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onAudioDisabled(DecoderCounters counters) { public final void onAudioDisabled(DecoderCounters counters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
@ -361,7 +365,7 @@ public class AnalyticsCollector
// VideoRendererEventListener implementation. // VideoRendererEventListener implementation.
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onVideoEnabled(DecoderCounters counters) { public final void onVideoEnabled(DecoderCounters counters) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -374,7 +378,7 @@ public class AnalyticsCollector
}); });
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onVideoDecoderInitialized( public final void onVideoDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) { String decoderName, long initializedTimestampMs, long initializationDurationMs) {
@ -384,12 +388,14 @@ public class AnalyticsCollector
AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED, AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED,
listener -> { listener -> {
listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs); listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs);
listener.onVideoDecoderInitialized(
eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
listener.onDecoderInitialized( listener.onDecoderInitialized(
eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
}); });
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onVideoInputFormatChanged( public final void onVideoInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
@ -398,6 +404,7 @@ public class AnalyticsCollector
eventTime, eventTime,
AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED, AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED,
listener -> { listener -> {
listener.onVideoInputFormatChanged(eventTime, format);
listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation);
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
}); });
@ -421,7 +428,7 @@ public class AnalyticsCollector
listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); listener -> listener.onVideoDecoderReleased(eventTime, decoderName));
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onVideoDisabled(DecoderCounters counters) { public final void onVideoDisabled(DecoderCounters counters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
@ -446,13 +453,17 @@ public class AnalyticsCollector
eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio));
} }
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onRenderedFirstFrame(@Nullable Surface surface) { public final void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
eventTime, eventTime,
AnalyticsListener.EVENT_RENDERED_FIRST_FRAME, AnalyticsListener.EVENT_RENDERED_FIRST_FRAME,
listener -> listener.onRenderedFirstFrame(eventTime, surface)); listener -> {
listener.onRenderedFirstFrame(eventTime, surface);
listener.onRenderedFirstFrame(eventTime, surface, renderTimeMs);
});
} }
@Override @Override
@ -615,16 +626,20 @@ public class AnalyticsCollector
listener -> listener.onStaticMetadataChanged(eventTime, metadataList)); listener -> listener.onStaticMetadataChanged(eventTime, metadataList));
} }
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onIsLoadingChanged(boolean isLoading) { public final void onIsLoadingChanged(boolean isLoading) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent( sendEvent(
eventTime, eventTime,
AnalyticsListener.EVENT_IS_LOADING_CHANGED, 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 @Override
public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
@ -725,7 +740,7 @@ public class AnalyticsCollector
listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters)); listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters));
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
@Override @Override
public final void onSeekProcessed() { public final void onSeekProcessed() {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
@ -747,12 +762,17 @@ public class AnalyticsCollector
// DefaultDrmSessionManager.EventListener implementation. // DefaultDrmSessionManager.EventListener implementation.
@Override @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); EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
sendEvent( sendEvent(
eventTime, eventTime,
AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED, AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED,
listener -> listener.onDrmSessionAcquired(eventTime)); listener -> {
listener.onDrmSessionAcquired(eventTime);
listener.onDrmSessionAcquired(eventTime, state);
});
} }
@Override @Override

View File

@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CodecException;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.IntDef; 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.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderException; import com.google.android.exoplayer2.decoder.DecoderException;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; 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.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaLoadData;
@ -583,10 +585,7 @@ public interface AnalyticsListener {
* @param eventTime The event time. * @param eventTime The event time.
* @param isLoading Whether the player is loading. * @param isLoading Whether the player is loading.
*/ */
@SuppressWarnings("deprecation") default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {}
default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {
onLoadingChanged(eventTime, isLoading);
}
/** @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead. */ /** @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead. */
@Deprecated @Deprecated
@ -755,8 +754,18 @@ public interface AnalyticsListener {
* *
* @param eventTime The event time. * @param eventTime The event time.
* @param decoderName The decoder that was created. * @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. * @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( default void onAudioDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {} 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 * decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder. * have a decoder.
*/ */
@SuppressWarnings("deprecation")
default void onAudioInputFormatChanged( default void onAudioInputFormatChanged(
EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime,
onAudioInputFormatChanged(eventTime, format); Format format,
} @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
/** /**
* Called when the audio position has increased for the first time since the last pause or * 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 eventTime The event time.
* @param decoderName The decoder that was created. * @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. * @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( default void onVideoDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {} 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 * decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder. * have a decoder.
*/ */
@SuppressWarnings("deprecation")
default void onVideoInputFormatChanged( default void onVideoInputFormatChanged(
EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime,
onVideoInputFormatChanged(eventTime, format); Format format,
} @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
/** /**
* Called after video frames have been dropped. * Called after video frames have been dropped.
@ -992,7 +1009,13 @@ public interface AnalyticsListener {
* @param eventTime The event time. * @param eventTime The event time.
* @param surface The {@link Surface} to which a frame has been rendered, or {@code null} if the * @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}. * 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) {} default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {}
/** /**
@ -1026,12 +1049,17 @@ public interface AnalyticsListener {
*/ */
default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {} 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. * Called each time a drm session is acquired.
* *
* @param eventTime The event time. * @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. * 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 * decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder. * have a decoder.
*/ */
@SuppressWarnings("deprecation")
default void onAudioInputFormatChanged( default void onAudioInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
onAudioInputFormatChanged(format);
}
/** /**
* Called when the audio position has increased for the first time since the last pause or * 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)}. */ /** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */
@SuppressWarnings("deprecation") // Calling deprecated listener method.
public void inputFormatChanged( public void inputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
if (handler != null) { if (handler != null) {
handler.post( 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. * 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); void setVolume(float volume);

View File

@ -215,6 +215,35 @@ public final class DefaultAudioSink implements AudioSink {
/** The default skip silence flag. */ /** The default skip silence flag. */
private static final boolean DEFAULT_SKIP_SILENCE = false; 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 @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH}) @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 AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints; private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
private final boolean enableAudioTrackPlaybackParams; private final boolean enableAudioTrackPlaybackParams;
private final boolean enableOffload; @OffloadMode private final int offloadMode;
@MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29; @MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29;
private final PendingExceptionHolder<InitializationException> private final PendingExceptionHolder<InitializationException>
initializationExceptionPendingExceptionHolder; initializationExceptionPendingExceptionHolder;
@ -364,7 +393,7 @@ public final class DefaultAudioSink implements AudioSink {
new DefaultAudioProcessorChain(audioProcessors), new DefaultAudioProcessorChain(audioProcessors),
enableFloatOutput, enableFloatOutput,
/* enableAudioTrackPlaybackParams= */ false, /* enableAudioTrackPlaybackParams= */ false,
/* enableOffload= */ false); OFFLOAD_MODE_DISABLED);
} }
/** /**
@ -382,8 +411,8 @@ public final class DefaultAudioSink implements AudioSink {
* use. * use.
* @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link * @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
* android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported. * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
* @param enableOffload Whether to enable audio offload. If an audio format can be both played * @param offloadMode Audio offload configuration. If an audio format can be both played with
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is * 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 * 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 * 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, * 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, AudioProcessorChain audioProcessorChain,
boolean enableFloatOutput, boolean enableFloatOutput,
boolean enableAudioTrackPlaybackParams, boolean enableAudioTrackPlaybackParams,
boolean enableOffload) { @OffloadMode int offloadMode) {
this.audioCapabilities = audioCapabilities; this.audioCapabilities = audioCapabilities;
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput; this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput;
this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams; 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); releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
@ -462,9 +491,7 @@ public final class DefaultAudioSink implements AudioSink {
// guaranteed to support. // guaranteed to support.
return SINK_FORMAT_SUPPORTED_WITH_TRANSCODING; return SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
} }
if (enableOffload if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) {
&& !offloadDisabledUntilNextConfiguration
&& isOffloadedPlaybackSupported(format, audioAttributes)) {
return SINK_FORMAT_SUPPORTED_DIRECTLY; return SINK_FORMAT_SUPPORTED_DIRECTLY;
} }
if (isPassthroughPlaybackSupported(format, audioCapabilities)) { if (isPassthroughPlaybackSupported(format, audioCapabilities)) {
@ -541,7 +568,7 @@ public final class DefaultAudioSink implements AudioSink {
availableAudioProcessors = new AudioProcessor[0]; availableAudioProcessors = new AudioProcessor[0];
outputSampleRate = inputFormat.sampleRate; outputSampleRate = inputFormat.sampleRate;
outputPcmFrameSize = C.LENGTH_UNSET; outputPcmFrameSize = C.LENGTH_UNSET;
if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) { if (useOffloadedPlayback(inputFormat, audioAttributes)) {
outputMode = OUTPUT_MODE_OFFLOAD; outputMode = OUTPUT_MODE_OFFLOAD;
outputEncoding = outputEncoding =
MimeTypes.getEncoding( MimeTypes.getEncoding(
@ -1478,6 +1505,10 @@ public final class DefaultAudioSink implements AudioSink {
&& !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) { && !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) {
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
encoding = C.ENCODING_E_AC3; 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)) { if (!audioCapabilities.supportsEncoding(encoding)) {
return null; return null;
@ -1561,9 +1592,8 @@ public final class DefaultAudioSink implements AudioSink {
return Util.getAudioTrackChannelConfig(channelCount); return Util.getAudioTrackChannelConfig(channelCount);
} }
private static boolean isOffloadedPlaybackSupported( private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) {
Format format, AudioAttributes audioAttributes) { if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) {
if (Util.SDK_INT < 29) {
return false; return false;
} }
@C.Encoding @C.Encoding
@ -1581,8 +1611,12 @@ public final class DefaultAudioSink implements AudioSink {
audioFormat, audioAttributes.getAudioAttributesV21())) { audioFormat, audioAttributes.getAudioAttributesV21())) {
return false; return false;
} }
boolean notGapless = format.encoderDelay == 0 && format.encoderPadding == 0; boolean isGapless = format.encoderDelay != 0 || format.encoderPadding != 0;
return notGapless || isOffloadedGaplessPlaybackSupported(); boolean offloadRequiresGaplessSupport = offloadMode == OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED;
if (isGapless && offloadRequiresGaplessSupport && !isOffloadedGaplessPlaybackSupported()) {
return false;
}
return true;
} }
private static boolean isOffloadedPlayback(AudioTrack audioTrack) { private static boolean isOffloadedPlayback(AudioTrack audioTrack) {

View File

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

View File

@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer2.drm; 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.annotation.SuppressLint;
import android.media.ResourceBusyException; import android.media.ResourceBusyException;
import android.os.Handler; 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.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; 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.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -47,9 +50,16 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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) @RequiresApi(18)
public class DefaultDrmSessionManager implements DrmSessionManager { public class DefaultDrmSessionManager implements DrmSessionManager {
@ -120,8 +130,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
*/ */
public Builder setUuidAndExoMediaDrmProvider( public Builder setUuidAndExoMediaDrmProvider(
UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {
this.uuid = Assertions.checkNotNull(uuid); this.uuid = checkNotNull(uuid);
this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider); this.exoMediaDrmProvider = checkNotNull(exoMediaDrmProvider);
return this; return this;
} }
@ -157,8 +167,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
public Builder setUseDrmSessionsForClearContent( public Builder setUseDrmSessionsForClearContent(
int... useDrmSessionsForClearContentTrackTypes) { int... useDrmSessionsForClearContentTrackTypes) {
for (int trackType : useDrmSessionsForClearContentTrackTypes) { for (int trackType : useDrmSessionsForClearContentTrackTypes) {
Assertions.checkArgument( checkArgument(trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
} }
this.useDrmSessionsForClearContentTrackTypes = this.useDrmSessionsForClearContentTrackTypes =
useDrmSessionsForClearContentTrackTypes.clone(); useDrmSessionsForClearContentTrackTypes.clone();
@ -185,7 +194,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* @return This builder. * @return This builder.
*/ */
public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy); this.loadErrorHandlingPolicy = checkNotNull(loadErrorHandlingPolicy);
return this; return this;
} }
@ -205,7 +214,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* @return This builder. * @return This builder.
*/ */
public Builder setSessionKeepaliveMs(long sessionKeepaliveMs) { public Builder setSessionKeepaliveMs(long sessionKeepaliveMs) {
Assertions.checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET); checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET);
this.sessionKeepaliveMs = sessionKeepaliveMs; this.sessionKeepaliveMs = sessionKeepaliveMs;
return this; return this;
} }
@ -282,14 +291,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private final List<DefaultDrmSession> sessions; private final List<DefaultDrmSession> sessions;
private final List<DefaultDrmSession> provisioningSessions; private final List<DefaultDrmSession> provisioningSessions;
private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
private final Set<DefaultDrmSession> keepaliveSessions; private final Set<DefaultDrmSession> keepaliveSessions;
private int prepareCallsCount; private int prepareCallsCount;
@Nullable private ExoMediaDrm exoMediaDrm; @Nullable private ExoMediaDrm exoMediaDrm;
@Nullable private DefaultDrmSession placeholderDrmSession; @Nullable private DefaultDrmSession placeholderDrmSession;
@Nullable private DefaultDrmSession noMultiSessionDrmSession; @Nullable private DefaultDrmSession noMultiSessionDrmSession;
@Nullable private Looper playbackLooper; private @MonotonicNonNull Looper playbackLooper;
private @MonotonicNonNull Handler sessionReleasingHandler; private @MonotonicNonNull Handler playbackHandler;
private int mode; private int mode;
@Nullable private byte[] offlineLicenseKeySetId; @Nullable private byte[] offlineLicenseKeySetId;
@ -388,8 +398,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
boolean playClearSamplesWithoutKeys, boolean playClearSamplesWithoutKeys,
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
long sessionKeepaliveMs) { long sessionKeepaliveMs) {
Assertions.checkNotNull(uuid); checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid; this.uuid = uuid;
this.exoMediaDrmProvider = exoMediaDrmProvider; this.exoMediaDrmProvider = exoMediaDrmProvider;
this.callback = callback; this.callback = callback;
@ -403,6 +413,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
mode = MODE_PLAYBACK; mode = MODE_PLAYBACK;
sessions = new ArrayList<>(); sessions = new ArrayList<>();
provisioningSessions = new ArrayList<>(); provisioningSessions = new ArrayList<>();
preacquiredSessionReferences = Sets.newIdentityHashSet();
keepaliveSessions = Sets.newIdentityHashSet(); keepaliveSessions = Sets.newIdentityHashSet();
this.sessionKeepaliveMs = sessionKeepaliveMs; 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. * @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) { public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {
Assertions.checkState(sessions.isEmpty()); checkState(sessions.isEmpty());
if (mode == MODE_QUERY || mode == MODE_RELEASE) { if (mode == MODE_QUERY || mode == MODE_RELEASE) {
Assertions.checkNotNull(offlineLicenseKeySetId); checkNotNull(offlineLicenseKeySetId);
} }
this.mode = mode; this.mode = mode;
this.offlineLicenseKeySetId = offlineLicenseKeySetId; this.offlineLicenseKeySetId = offlineLicenseKeySetId;
@ -447,7 +458,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (prepareCallsCount++ != 0) { if (prepareCallsCount++ != 0) {
return; return;
} }
Assertions.checkState(exoMediaDrm == null); checkState(exoMediaDrm == null);
exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
} }
@ -466,10 +477,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
sessions.get(i).release(/* eventDispatcher= */ null); sessions.get(i).release(/* eventDispatcher= */ null);
} }
} }
Assertions.checkNotNull(exoMediaDrm).release(); releaseAllPreacquiredSessions();
checkNotNull(exoMediaDrm).release();
exoMediaDrm = null; 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 @Override
@Nullable @Nullable
public DrmSession acquireSession( public DrmSession acquireSession(
@ -477,16 +502,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) { Format format) {
initPlaybackLooper(playbackLooper); 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); maybeCreateMediaDrmHandler(playbackLooper);
if (format.drmInitData == null) { if (format.drmInitData == null) {
// Content is not encrypted. // Content is not encrypted.
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType)); return maybeAcquirePlaceholderSession(
MimeTypes.getTrackType(format.sampleMimeType),
shouldReleasePreacquiredSessionsBeforeRetrying);
} }
@Nullable List<SchemeData> schemeDatas = null; @Nullable List<SchemeData> schemeDatas = null;
if (offlineLicenseKeySetId == null) { if (offlineLicenseKeySetId == null) {
schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false); schemeDatas = getSchemeDatas(checkNotNull(format.drmInitData), uuid, false);
if (schemeDatas.isEmpty()) { if (schemeDatas.isEmpty()) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid); final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
Log.e(TAG, "DRM error", error); Log.e(TAG, "DRM error", error);
@ -515,7 +556,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Create a new session. // Create a new session.
session = session =
createAndAcquireSessionWithRetry( createAndAcquireSessionWithRetry(
schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher); schemeDatas,
/* isPlaceholderSession= */ false,
eventDispatcher,
shouldReleasePreacquiredSessionsBeforeRetrying);
if (!multiSession) { if (!multiSession) {
noMultiSessionDrmSession = session; noMultiSessionDrmSession = session;
} }
@ -531,7 +575,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable @Nullable
public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format) { public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format) {
Class<? extends ExoMediaCrypto> exoMediaCryptoType = Class<? extends ExoMediaCrypto> exoMediaCryptoType =
Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType(); checkNotNull(exoMediaDrm).getExoMediaCryptoType();
if (format.drmInitData == null) { if (format.drmInitData == null) {
int trackType = MimeTypes.getTrackType(format.sampleMimeType); int trackType = MimeTypes.getTrackType(format.sampleMimeType);
return Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) != C.INDEX_UNSET return Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) != C.INDEX_UNSET
@ -547,8 +591,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Internal methods. // Internal methods.
@Nullable @Nullable
private DrmSession maybeAcquirePlaceholderSession(int trackType) { private DrmSession maybeAcquirePlaceholderSession(
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
ExoMediaDrm exoMediaDrm = checkNotNull(this.exoMediaDrm);
boolean avoidPlaceholderDrmSessions = boolean avoidPlaceholderDrmSessions =
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
@ -563,7 +608,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
createAndAcquireSessionWithRetry( createAndAcquireSessionWithRetry(
/* schemeDatas= */ ImmutableList.of(), /* schemeDatas= */ ImmutableList.of(),
/* isPlaceholderSession= */ true, /* isPlaceholderSession= */ true,
/* eventDispatcher= */ null); /* eventDispatcher= */ null,
shouldReleasePreacquiredSessionsBeforeRetrying);
sessions.add(placeholderDrmSession); sessions.add(placeholderDrmSession);
this.placeholderDrmSession = placeholderDrmSession; this.placeholderDrmSession = placeholderDrmSession;
} else { } else {
@ -607,12 +653,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
return true; return true;
} }
private void initPlaybackLooper(Looper playbackLooper) { @EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"})
private synchronized void initPlaybackLooper(Looper playbackLooper) {
if (this.playbackLooper == null) { if (this.playbackLooper == null) {
this.playbackLooper = playbackLooper; this.playbackLooper = playbackLooper;
this.sessionReleasingHandler = new Handler(playbackLooper); this.playbackHandler = new Handler(playbackLooper);
} else { } else {
Assertions.checkState(this.playbackLooper == playbackLooper); checkState(this.playbackLooper == playbackLooper);
checkNotNull(playbackHandler);
} }
} }
@ -625,35 +673,67 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private DefaultDrmSession createAndAcquireSessionWithRetry( private DefaultDrmSession createAndAcquireSessionWithRetry(
@Nullable List<SchemeData> schemeDatas, @Nullable List<SchemeData> schemeDatas,
boolean isPlaceholderSession, boolean isPlaceholderSession,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
DefaultDrmSession session = DefaultDrmSession session =
createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
if (session.getState() == DrmSession.STATE_ERROR // If we're short on DRM session resources, first try eagerly releasing all our keepalive
&& (Util.SDK_INT < 19 // sessions and then retry the acquisition.
|| Assertions.checkNotNull(session.getError()).getCause() if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) {
instanceof ResourceBusyException)) { // Make a local copy, because sessions are removed from this.keepaliveSessions during
// We're short on DRM session resources, so eagerly release all our keepalive sessions. // release (via callback).
// ResourceBusyException is only available at API 19, so on earlier versions we always ImmutableSet<DefaultDrmSession> keepaliveSessions =
// eagerly release regardless of the underlying error. ImmutableSet.copyOf(this.keepaliveSessions);
if (!keepaliveSessions.isEmpty()) { for (DrmSession keepaliveSession : keepaliveSessions) {
// Make a local copy, because sessions are removed from this.keepaliveSessions during keepaliveSession.release(/* eventDispatcher= */ null);
// 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);
} }
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; 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 * Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in
* {@code eventDispatcher}). * {@code eventDispatcher}).
@ -665,7 +745,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable List<SchemeData> schemeDatas, @Nullable List<SchemeData> schemeDatas,
boolean isPlaceholderSession, boolean isPlaceholderSession,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
Assertions.checkNotNull(exoMediaDrm); checkNotNull(exoMediaDrm);
// Placeholder sessions should always play clear samples without keys. // Placeholder sessions should always play clear samples without keys.
boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;
DefaultDrmSession session = DefaultDrmSession session =
@ -681,7 +761,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
offlineLicenseKeySetId, offlineLicenseKeySetId,
keyRequestParameters, keyRequestParameters,
callback, callback,
Assertions.checkNotNull(playbackLooper), checkNotNull(playbackLooper),
loadErrorHandlingPolicy); loadErrorHandlingPolicy);
// Acquire the session once on behalf of the caller to DrmSessionManager - this is the // 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, // 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) { if (sessionKeepaliveMs != C.TIME_UNSET) {
// The session has been acquired elsewhere so we want to cancel our timeout. // The session has been acquired elsewhere so we want to cancel our timeout.
keepaliveSessions.remove(session); 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) { if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) {
// Only the internal keep-alive reference remains, so we can start the timeout. // Only the internal keep-alive reference remains, so we can start the timeout.
keepaliveSessions.add(session); keepaliveSessions.add(session);
Assertions.checkNotNull(sessionReleasingHandler) checkNotNull(playbackHandler)
.postAtTime( .postAtTime(
() -> session.release(/* eventDispatcher= */ null), () -> session.release(/* eventDispatcher= */ null),
session, session,
@ -812,7 +892,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
} }
provisioningSessions.remove(session); provisioningSessions.remove(session);
if (sessionKeepaliveMs != C.TIME_UNSET) { if (sessionKeepaliveMs != C.TIME_UNSET) {
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session); checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
keepaliveSessions.remove(session); keepaliveSessions.remove(session);
} }
} }
@ -824,7 +904,78 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Override @Override
public void onEvent( public void onEvent(
ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) { 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. */ /** Listener of {@link DrmSessionManager} events. */
public interface DrmSessionEventListener { 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. * Called each time a drm session is acquired.
* *
* @param windowIndex The window index in the timeline this media period belongs to. * @param windowIndex The window index in the timeline this media period belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session. * @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. * 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) { for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
DrmSessionEventListener listener = listenerAndHandler.listener; DrmSessionEventListener listener = listenerAndHandler.listener;
postOrRun( postOrRun(
listenerAndHandler.handler, 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. */ /** Manages a DRM session. */
public interface DrmSessionManager { 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. */ /** An instance that supports no DRM schemes. */
DrmSessionManager DRM_UNSUPPORTED = DrmSessionManager DRM_UNSUPPORTED =
new DrmSessionManager() { new DrmSessionManager() {
@ -81,6 +98,51 @@ public interface DrmSessionManager {
// Do nothing. // 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 * 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 * 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 { throws ExoPlaybackException {
this.currentPlaybackSpeed = currentPlaybackSpeed; this.currentPlaybackSpeed = currentPlaybackSpeed;
this.targetPlaybackSpeed = targetPlaybackSpeed; this.targetPlaybackSpeed = targetPlaybackSpeed;
if (codec != null updateCodecOperatingRate(codecInputFormat);
&& codecDrainAction != DRAIN_ACTION_REINITIALIZE
&& getState() != STATE_DISABLED) {
updateCodecOperatingRate(codecInputFormat);
}
} }
@Override @Override
@ -1689,6 +1685,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return CODEC_OPERATING_RATE_UNSET; 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 * Updates the codec operating rate, or triggers codec release and re-initialization if a
* previously set operating rate needs to be cleared. * previously set operating rate needs to be cleared.
@ -1702,6 +1709,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return true; return true;
} }
if (codec == null
|| codecDrainAction == DRAIN_ACTION_REINITIALIZE
|| getState() == STATE_DISABLED) {
// No need to update the operating rate.
return true;
}
float newCodecOperatingRate = float newCodecOperatingRate =
getCodecOperatingRateV23(targetPlaybackSpeed, format, getStreamFormats()); getCodecOperatingRateV23(targetPlaybackSpeed, format, getStreamFormats());
if (codecOperatingRate == newCodecOperatingRate) { if (codecOperatingRate == newCodecOperatingRate) {

View File

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

View File

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

View File

@ -26,7 +26,14 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** Extracts the contents of a container file from a progressive media stream. */ /** 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. * 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.drm.DrmSessionManager;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; 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 uri The {@link Uri} of the media stream.
* @param dataSource The data source to read the media. * @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 drmSessionManager A {@link DrmSessionManager} to allow DRM interactions.
* @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events. * @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events.
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
@ -168,7 +168,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public ProgressiveMediaPeriod( public ProgressiveMediaPeriod(
Uri uri, Uri uri,
DataSource dataSource, DataSource dataSource,
ExtractorsFactory extractorsFactory, ProgressiveMediaExtractor progressiveMediaExtractor,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher, DrmSessionEventListener.EventDispatcher drmEventDispatcher,
LoadErrorHandlingPolicy loadErrorHandlingPolicy, LoadErrorHandlingPolicy loadErrorHandlingPolicy,
@ -188,7 +188,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.customCacheKey = customCacheKey; this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("ProgressiveMediaPeriod"); loader = new Loader("ProgressiveMediaPeriod");
this.progressiveMediaExtractor = new BundledExtractorsAdapter(extractorsFactory); this.progressiveMediaExtractor = progressiveMediaExtractor;
loadCondition = new ConditionVariable(); loadCondition = new ConditionVariable();
maybeFinishPrepareRunnable = this::maybeFinishPrepare; maybeFinishPrepareRunnable = this::maybeFinishPrepare;
onContinueLoadingRequestedRunnable = onContinueLoadingRequestedRunnable =

View File

@ -54,7 +54,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private ExtractorsFactory extractorsFactory; private ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
private boolean usingCustomDrmSessionManagerProvider; private boolean usingCustomDrmSessionManagerProvider;
private DrmSessionManagerProvider drmSessionManagerProvider; private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
@ -72,15 +72,26 @@ public final class ProgressiveMediaSource extends BaseMediaSource
this(dataSourceFactory, new DefaultExtractorsFactory()); 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. * Creates a new factory for {@link ProgressiveMediaSource}s.
* *
* @param dataSourceFactory A factory for {@link DataSource}s to read the media. * @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.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory; this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
drmSessionManagerProvider = new DefaultDrmSessionManagerProvider(); drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
@ -93,8 +104,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
*/ */
@Deprecated @Deprecated
public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) { public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) {
this.extractorsFactory = this.progressiveMediaExtractorFactory =
extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory(); () ->
new BundledExtractorsAdapter(
extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory());
return this; return this;
} }
@ -220,7 +233,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return new ProgressiveMediaSource( return new ProgressiveMediaSource(
mediaItem, mediaItem,
dataSourceFactory, dataSourceFactory,
extractorsFactory, progressiveMediaExtractorFactory,
drmSessionManagerProvider.get(mediaItem), drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy, loadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes); continueLoadingCheckIntervalBytes);
@ -241,7 +254,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
private final MediaItem mediaItem; private final MediaItem mediaItem;
private final MediaItem.PlaybackProperties playbackProperties; private final MediaItem.PlaybackProperties playbackProperties;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory; private final ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
private final DrmSessionManager drmSessionManager; private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
private final int continueLoadingCheckIntervalBytes; private final int continueLoadingCheckIntervalBytes;
@ -256,14 +269,14 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/* package */ ProgressiveMediaSource( /* package */ ProgressiveMediaSource(
MediaItem mediaItem, MediaItem mediaItem,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
int continueLoadingCheckIntervalBytes) { int continueLoadingCheckIntervalBytes) {
this.playbackProperties = checkNotNull(mediaItem.playbackProperties); this.playbackProperties = checkNotNull(mediaItem.playbackProperties);
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory; this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
@ -308,7 +321,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
return new ProgressiveMediaPeriod( return new ProgressiveMediaPeriod(
playbackProperties.uri, playbackProperties.uri,
dataSource, dataSource,
extractorsFactory, progressiveMediaExtractorFactory.createProgressiveMediaExtractor(),
drmSessionManager, drmSessionManager,
createDrmEventDispatcher(id), createDrmEventDispatcher(id),
loadableLoadErrorHandlingPolicy, 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.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; 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.upstream.DataReader;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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 { 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 static final PositionHolder POSITION_HOLDER = new PositionHolder();
private final Extractor extractor; 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.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import java.io.IOException; import java.io.IOException;
import java.util.List;
/** /**
* Extracts samples and track {@link Format Formats} from chunks. * Extracts samples and track {@link Format Formats} from chunks.
@ -31,6 +32,27 @@ import java.io.IOException;
*/ */
public interface ChunkExtractor { 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. */ /** Provides {@link TrackOutput} instances to be written to during extraction. */
interface TrackOutputProvider { interface TrackOutputProvider {

View File

@ -26,6 +26,7 @@ import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.P
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.media.MediaParser; import android.media.MediaParser;
import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
@ -49,6 +50,25 @@ import java.util.List;
@RequiresApi(30) @RequiresApi(30)
public final class MediaParserChunkExtractor implements ChunkExtractor { 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 OutputConsumerAdapterV30 outputConsumerAdapter;
private final InputReaderAdapterV30 inputReaderAdapter; private final InputReaderAdapterV30 inputReaderAdapter;
private final MediaParser mediaParser; 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) { if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) {
cue.setTextSize( cue.setTextSize(
style.fontSize / screenHeight, style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
} }
if (style.bold && style.italic) { if (style.bold && style.italic) {
spannableText.setSpan( spannableText.setSpan(

View File

@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util;
break; 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) ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length)
: null; : null;
} }

View File

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

View File

@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition; import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition;
import com.google.android.exoplayer2.util.MimeTypes;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Track selection related utility methods. */ /** Track selection related utility methods. */
@ -97,4 +98,20 @@ public final class TrackSelectionUtil {
} }
return builder.build(); 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) { if (skipped < dataSpec.position) {
// assetManager.open() returns an AssetInputStream, whose skip() implementation only skips // 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. // 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) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;

View File

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

View File

@ -80,7 +80,7 @@ public final class ContentDataSource extends BaseDataSource {
if (skipped != 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 // 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. // skip beyond the end of the data.
throw new EOFException(); throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
} }
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;
@ -96,13 +96,13 @@ public final class ContentDataSource extends BaseDataSource {
} else { } else {
bytesRemaining = channelSize - channel.position(); bytesRemaining = channelSize - channel.position();
if (bytesRemaining < 0) { if (bytesRemaining < 0) {
throw new EOFException(); throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
} }
} }
} else { } else {
bytesRemaining = assetFileDescriptorLength - skipped; bytesRemaining = assetFileDescriptorLength - skipped;
if (bytesRemaining < 0) { 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 = endPosition =
dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;
if (endPosition > data.length || readPosition > endPosition) { if (readPosition >= endPosition) {
data = null; data = null;
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
} }

View File

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

View File

@ -60,7 +60,7 @@ public final class RawResourceDataSource extends BaseDataSource {
super(message); super(message);
} }
public RawResourceDataSourceException(IOException e) { public RawResourceDataSourceException(Throwable e) {
super(e); super(e);
} }
} }
@ -133,21 +133,39 @@ public final class RawResourceDataSource extends BaseDataSource {
} }
transferInitializing(dataSpec); 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; this.assetFileDescriptor = assetFileDescriptor;
if (assetFileDescriptor == null) { if (assetFileDescriptor == null) {
throw new RawResourceDataSourceException("Resource is compressed: " + uri); throw new RawResourceDataSourceException("Resource is compressed: " + uri);
} }
long assetFileDescriptorLength = assetFileDescriptor.getLength();
FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
this.inputStream = inputStream; this.inputStream = inputStream;
try { 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()); inputStream.skip(assetFileDescriptor.getStartOffset());
long skipped = inputStream.skip(dataSpec.position); long skipped = inputStream.skip(dataSpec.position);
if (skipped < 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 // 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. // read beyond the end of the last resource in the file.
throw new EOFException(); throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
} }
} catch (IOException e) { } catch (IOException e) {
throw new RawResourceDataSourceException(e); throw new RawResourceDataSourceException(e);
@ -156,7 +174,6 @@ public final class RawResourceDataSource extends BaseDataSource {
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;
} else { } else {
long assetFileDescriptorLength = assetFileDescriptor.getLength();
// If the length is UNKNOWN_LENGTH then the asset extends to the end of the file. // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
bytesRemaining = bytesRemaining =
assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH

View File

@ -55,7 +55,6 @@ public final class CacheWriter {
private final byte[] temporaryBuffer; private final byte[] temporaryBuffer;
@Nullable private final ProgressListener progressListener; @Nullable private final ProgressListener progressListener;
private boolean initialized;
private long nextPosition; private long nextPosition;
private long endPosition; private long endPosition;
private long bytesCached; private long bytesCached;
@ -118,18 +117,15 @@ public final class CacheWriter {
public void cache() throws IOException { public void cache() throws IOException {
throwIfCanceled(); throwIfCanceled();
if (!initialized) { bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length);
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
endPosition = dataSpec.position + dataSpec.length; endPosition = dataSpec.position + dataSpec.length;
} else { } else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey)); long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey));
endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength; endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength;
} }
bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length); if (progressListener != null) {
if (progressListener != null) { progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0);
progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0);
}
initialized = true;
} }
while (endPosition == C.POSITION_UNSET || nextPosition < endPosition) { while (endPosition == C.POSITION_UNSET || nextPosition < endPosition) {
@ -158,42 +154,50 @@ public final class CacheWriter {
*/ */
private long readBlockToCache(long position, long length) throws IOException { private long readBlockToCache(long position, long length) throws IOException {
boolean isLastBlock = position + length == endPosition || length == C.LENGTH_UNSET; boolean isLastBlock = position + length == endPosition || length == C.LENGTH_UNSET;
try {
long resolvedLength = C.LENGTH_UNSET; long resolvedLength = C.LENGTH_UNSET;
boolean isDataSourceOpen = false; boolean isDataSourceOpen = false;
if (length != C.LENGTH_UNSET) { if (length != C.LENGTH_UNSET) {
// If the length is specified, try to open the data source with a bounded request to avoid // 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. // the underlying network stack requesting more data than required.
try { DataSpec boundedDataSpec =
DataSpec boundedDataSpec = dataSpec.buildUpon().setPosition(position).setLength(length).build();
dataSpec.buildUpon().setPosition(position).setLength(length).build(); try {
resolvedLength = dataSource.open(boundedDataSpec); resolvedLength = dataSource.open(boundedDataSpec);
isDataSourceOpen = true; isDataSourceOpen = true;
} catch (IOException exception) { } catch (IOException e) {
if (allowShortContent Util.closeQuietly(dataSource);
&& isLastBlock if (allowShortContent
&& DataSourceException.isCausedByPositionOutOfRange(exception)) { && isLastBlock
// The length of the request exceeds the length of the content. If we allow shorter && DataSourceException.isCausedByPositionOutOfRange(e)) {
// content and are reading the last block, fall through and try again with an unbounded // The length of the request exceeds the length of the content. If we allow shorter
// request to read up to the end of the content. // content and are reading the last block, fall through and try again with an unbounded
Util.closeQuietly(dataSource); // request to read up to the end of the content.
} else { } else {
throw exception; 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. if (!isDataSourceOpen) {
throwIfCanceled(); // Either the length was unspecified, or we allow short content and our attempt to open the
DataSpec unboundedDataSpec = // DataSource with the specified length failed.
dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build(); throwIfCanceled();
DataSpec unboundedDataSpec =
dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build();
try {
resolvedLength = dataSource.open(unboundedDataSpec); resolvedLength = dataSource.open(unboundedDataSpec);
} catch (IOException e) {
Util.closeQuietly(dataSource);
throw e;
} }
}
int totalBytesRead = 0;
try {
if (isLastBlock && resolvedLength != C.LENGTH_UNSET) { if (isLastBlock && resolvedLength != C.LENGTH_UNSET) {
onRequestEndPosition(position + resolvedLength); onRequestEndPosition(position + resolvedLength);
} }
int totalBytesRead = 0;
int bytesRead = 0; int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) { while (bytesRead != C.RESULT_END_OF_INPUT) {
throwIfCanceled(); throwIfCanceled();
@ -206,10 +210,16 @@ public final class CacheWriter {
if (isLastBlock) { if (isLastBlock) {
onRequestEndPosition(position + totalBytesRead); onRequestEndPosition(position + totalBytesRead);
} }
return totalBytesRead; } catch (IOException e) {
} finally {
Util.closeQuietly(dataSource); 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) { 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.audio.AudioAttributes;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; 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.metadata.Metadata;
import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaLoadData;
@ -479,8 +480,8 @@ public class EventLogger implements AnalyticsListener {
} }
@Override @Override
public void onDrmSessionAcquired(EventTime eventTime) { public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {
logd(eventTime, "drmSessionAcquired"); logd(eventTime, "drmSessionAcquired", "state=" + state);
} }
@Override @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 * decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder. * have a decoder.
*/ */
@SuppressWarnings("deprecation")
default void onVideoInputFormatChanged( default void onVideoInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
onVideoInputFormatChanged(format);
}
/** /**
* Called to report the number of frames dropped by the renderer. Dropped frames are reported * 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 * @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}. * 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) {} default void onRenderedFirstFrame(@Nullable Surface surface) {}
/** /**
@ -205,11 +207,15 @@ public interface VideoRendererEventListener {
* Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format, * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format,
* DecoderReuseEvaluation)}. * DecoderReuseEvaluation)}.
*/ */
@SuppressWarnings("deprecation") // Calling deprecated listener method.
public void inputFormatChanged( public void inputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
if (handler != null) { if (handler != null) {
handler.post( 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) { public void renderedFirstFrame(@Nullable Surface surface) {
if (handler != null) { 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)); 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 @Test
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed() public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
throws Exception { 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.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmInitData; 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.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaDrm; import com.google.android.exoplayer2.drm.ExoMediaDrm;
import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.drm.MediaDrmCallback;
@ -1700,12 +1701,12 @@ public final class AnalyticsCollectorTest {
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce()) verify(listener, atLeastOnce())
.onVideoDecoderInitialized( .onVideoDecoderInitialized(
individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong()); individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong());
ArgumentCaptor<AnalyticsListener.EventTime> individualAudioDecoderInitializedEventTimes = ArgumentCaptor<AnalyticsListener.EventTime> individualAudioDecoderInitializedEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce()) verify(listener, atLeastOnce())
.onAudioDecoderInitialized( .onAudioDecoderInitialized(
individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong()); individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong());
ArgumentCaptor<AnalyticsListener.EventTime> individualVideoDisabledEventTimes = ArgumentCaptor<AnalyticsListener.EventTime> individualVideoDisabledEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce()) verify(listener, atLeastOnce())
@ -1717,7 +1718,7 @@ public final class AnalyticsCollectorTest {
ArgumentCaptor<AnalyticsListener.EventTime> individualRenderedFirstFrameEventTimes = ArgumentCaptor<AnalyticsListener.EventTime> individualRenderedFirstFrameEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce()) verify(listener, atLeastOnce())
.onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any()); .onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any(), anyLong());
ArgumentCaptor<AnalyticsListener.EventTime> individualVideoSizeChangedEventTimes = ArgumentCaptor<AnalyticsListener.EventTime> individualVideoSizeChangedEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce()) verify(listener, atLeastOnce())
@ -2183,7 +2184,10 @@ public final class AnalyticsCollectorTest {
@Override @Override
public void onAudioDecoderInitialized( 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)); reportedEvents.add(new ReportedEvent(EVENT_AUDIO_DECODER_INITIALIZED, eventTime));
} }
@ -2220,7 +2224,10 @@ public final class AnalyticsCollectorTest {
@Override @Override
public void onVideoDecoderInitialized( 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)); reportedEvents.add(new ReportedEvent(EVENT_VIDEO_DECODER_INITIALIZED, eventTime));
} }
@ -2246,7 +2253,8 @@ public final class AnalyticsCollectorTest {
} }
@Override @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)); reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime));
} }
@ -2261,7 +2269,7 @@ public final class AnalyticsCollectorTest {
} }
@Override @Override
public void onDrmSessionAcquired(EventTime eventTime) { public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {
reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_ACQUIRED, eventTime)); reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_ACQUIRED, eventTime));
} }

View File

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

View File

@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowLooper;
@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper;
// - Multiple acquisitions & releases for same keys -> multiple requests. // - Multiple acquisitions & releases for same keys -> multiple requests.
// - Provisioning. // - Provisioning.
// - Key denial. // - Key denial.
// - Handling of ResourceBusyException (indicating session scarcity).
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DefaultDrmSessionManagerTest { public class DefaultDrmSessionManagerTest {
@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest {
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); 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) { private static void waitForOpenedWithKeys(DrmSession drmSession) {
// Check the error first, so we get a meaningful failure if there's been an error. // Check the error first, so we get a meaningful failure if there's been an error.
assertThat(drmSession.getError()).isNull(); assertThat(drmSession.getError()).isNull();

View File

@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.database.DatabaseProvider; 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.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil; 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 com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -66,7 +68,7 @@ public class ProgressiveDownloaderTest {
} }
@Test @Test
public void download_afterSingleFailure_succeeds() throws Exception { public void download_afterReadFailure_succeeds() throws Exception {
Uri uri = Uri.parse("test:///test.mp4"); Uri uri = Uri.parse("test:///test.mp4");
// Fake data has a built in failure after 10 bytes. // Fake data has a built in failure after 10 bytes.
@ -92,6 +94,39 @@ public class ProgressiveDownloaderTest {
assertThat(progressListener.bytesDownloaded).isEqualTo(30); 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 { private static final class TestProgressListener implements Downloader.ProgressListener {
public long bytesDownloaded; 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.C;
import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager; 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.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.upstream.AssetDataSource; import com.google.android.exoplayer2.upstream.AssetDataSource;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** Unit test for {@link ProgressiveMediaPeriod}. */ /** Unit test for {@link ProgressiveMediaPeriod}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class ProgressiveMediaPeriodTest { public final class ProgressiveMediaPeriodTest {
@Test @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); AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false);
ProgressiveMediaPeriod.Listener sourceInfoRefreshListener = ProgressiveMediaPeriod.Listener sourceInfoRefreshListener =
(durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true); (durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true);
@ -48,7 +63,7 @@ public final class ProgressiveMediaPeriodTest {
new ProgressiveMediaPeriod( new ProgressiveMediaPeriod(
Uri.parse("asset://android_asset/media/mp4/sample.mp4"), Uri.parse("asset://android_asset/media/mp4/sample.mp4"),
new AssetDataSource(ApplicationProvider.getApplicationContext()), new AssetDataSource(ApplicationProvider.getApplicationContext()),
() -> new Extractor[] {new Mp4Extractor()}, extractor,
DrmSessionManager.DRM_UNSUPPORTED, DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher() new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId), .withParameters(/* windowIndex= */ 0, mediaPeriodId),

View File

@ -323,17 +323,18 @@ public final class SsaDecoderTest {
} }
@Test @Test
public void decodeFontSize() throws IOException{ public void decodeFontSize() throws IOException {
SsaDecoder decoder = new SsaDecoder(); 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); Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); 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); assertThat(firstCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); 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); 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); 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. * Tests reading from a {@link ByteArrayDataSource} with various parameters.
* *

View File

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

View File

@ -39,12 +39,12 @@ public class DataSchemeDataSourceContractTest extends DataSourceContractTest {
return ImmutableList.of( return ImmutableList.of(
new TestResource.Builder() new TestResource.Builder()
.setName("plain text") .setName("plain text")
.setUri(Uri.parse("data:text/plain," + DATA)) .setUri("data:text/plain," + DATA)
.setExpectedBytes(DATA.getBytes(UTF_8)) .setExpectedBytes(DATA.getBytes(UTF_8))
.build(), .build(),
new TestResource.Builder() new TestResource.Builder()
.setName("base64 encoded text") .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)) .setExpectedBytes(Base64.decode(BASE64_ENCODED_DATA, Base64.DEFAULT))
.build()); .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 @Test
public void incorrectScheme() { public void incorrectScheme() {
try { 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; return udpDataSource;
} }
@Override
protected boolean unboundedReadsAreIndefinite() {
return true;
}
@Override @Override
protected ImmutableList<TestResource> getTestResources() { protected ImmutableList<TestResource> getTestResources() {
return ImmutableList.of( return ImmutableList.of(
new TestResource.Builder() new TestResource.Builder()
.setName("local-udp-unicast-socket") .setName("local-udp-unicast-socket")
.setUri(Uri.parse("udp://localhost:" + findFreeUdpPort())) .setUri("udp://localhost:" + findFreeUdpPort())
.setExpectedBytes(data) .setExpectedBytes(data)
.setEndOfInputExpected(false)
.build()); .build());
} }
@ -84,6 +88,26 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
@Override @Override
public void dataSpecWithPositionAndLength_readExpectedRange() {} 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 * 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. * 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.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import android.net.Uri; import android.net.Uri;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; 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.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
/** Unit tests for {@link CacheWriter}. */ /** Unit tests for {@link CacheWriter}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class CacheWriterTest { 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 File tempFolder;
private SimpleCache cache; private SimpleCache cache;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mockCache.init();
tempFolder = tempFolder =
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest"); Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
cache = cache =
@ -219,6 +175,7 @@ public final class CacheWriterTest {
assertCachedData(cache, fakeDataSet); assertCachedData(cache, fakeDataSet);
} }
@Ignore("Currently broken. See https://github.com/google/ExoPlayer/issues/7326.")
@Test @Test
public void cacheLengthExceedsActualDataLength() throws Exception { public void cacheLengthExceedsActualDataLength() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
@ -263,6 +220,50 @@ public final class CacheWriterTest {
assertThat(DataSourceException.isCausedByPositionOutOfRange(exception)).isTrue(); 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 @Test
public void cachePolling() throws Exception { public void cachePolling() throws Exception {
final CachingCounters counters = new CachingCounters(); 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.Format;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.ChunkIndex; 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.BehindLiveWindowException;
import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor; 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.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -180,11 +174,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
representationHolders[i] = representationHolders[i] =
new RepresentationHolder( new RepresentationHolder(
periodDurationUs, periodDurationUs,
trackType,
representation, representation,
enableEventMessageTrack, BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor(
closedCaptionFormats, trackType,
playerTrackEmsgHandler); representation.format,
enableEventMessageTrack,
closedCaptionFormats,
playerTrackEmsgHandler),
/* segmentNumShift= */ 0,
representation.getIndex());
} }
} }
@ -665,26 +663,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
private final long segmentNumShift; private final long segmentNumShift;
/* package */ RepresentationHolder( /* 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, long periodDurationUs,
Representation representation, Representation representation,
@Nullable ChunkExtractor chunkExtractor, @Nullable ChunkExtractor chunkExtractor,
@ -800,40 +778,5 @@ public class DefaultDashChunkSource implements DashChunkSource {
public boolean isSegmentAvailableAtFullNetworkSpeed(long segmentNum, long nowPeriodTimeUs) { public boolean isSegmentAvailableAtFullNetworkSpeed(long segmentNum, long nowPeriodTimeUs) {
return nowPeriodTimeUs == C.TIME_UNSET || getSegmentEndTimeUs(segmentNum) <= 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); timeline.getWindow(player.getCurrentWindowIndex(), window);
boolean isSeekable = window.isSeekable; boolean isSeekable = window.isSeekable;
enableSeeking = isSeekable; enableSeeking = isSeekable;
enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious(); enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
enableRewind = isSeekable && controlDispatcher.isRewindEnabled(); enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled(); enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
enableNext = 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 fastForwardActionIconResourceId;
private int previousActionIconResourceId; private int previousActionIconResourceId;
private int nextActionIconResourceId; private int nextActionIconResourceId;
@Nullable private String groupKey;
/** /**
* Creates an instance. * Creates an instance.
@ -514,6 +515,18 @@ public class PlayerNotificationManager {
return this; 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}. */ /** Builds the {@link PlayerNotificationManager}. */
public PlayerNotificationManager build() { public PlayerNotificationManager build() {
if (channelNameResourceId != 0) { if (channelNameResourceId != 0) {
@ -538,7 +551,8 @@ public class PlayerNotificationManager {
rewindActionIconResourceId, rewindActionIconResourceId,
fastForwardActionIconResourceId, fastForwardActionIconResourceId,
previousActionIconResourceId, previousActionIconResourceId,
nextActionIconResourceId); nextActionIconResourceId,
groupKey);
} }
} }
@ -662,6 +676,7 @@ public class PlayerNotificationManager {
private int visibility; private int visibility;
@Priority private int priority; @Priority private int priority;
private boolean useChronometer; private boolean useChronometer;
@Nullable private String groupKey;
/** @deprecated Use the {@link Builder} instead. */ /** @deprecated Use the {@link Builder} instead. */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -805,7 +820,8 @@ public class PlayerNotificationManager {
R.drawable.exo_notification_rewind, R.drawable.exo_notification_rewind,
R.drawable.exo_notification_fastforward, R.drawable.exo_notification_fastforward,
R.drawable.exo_notification_previous, R.drawable.exo_notification_previous,
R.drawable.exo_notification_next); R.drawable.exo_notification_next,
null);
} }
private PlayerNotificationManager( private PlayerNotificationManager(
@ -822,7 +838,8 @@ public class PlayerNotificationManager {
int rewindActionIconResourceId, int rewindActionIconResourceId,
int fastForwardActionIconResourceId, int fastForwardActionIconResourceId,
int previousActionIconResourceId, int previousActionIconResourceId,
int nextActionIconResourceId) { int nextActionIconResourceId,
@Nullable String groupKey) {
context = context.getApplicationContext(); context = context.getApplicationContext();
this.context = context; this.context = context;
this.channelId = channelId; this.channelId = channelId;
@ -831,6 +848,7 @@ public class PlayerNotificationManager {
this.notificationListener = notificationListener; this.notificationListener = notificationListener;
this.customActionReceiver = customActionReceiver; this.customActionReceiver = customActionReceiver;
this.smallIconResourceId = smallIconResourceId; this.smallIconResourceId = smallIconResourceId;
this.groupKey = groupKey;
controlDispatcher = new DefaultControlDispatcher(); controlDispatcher = new DefaultControlDispatcher();
window = new Timeline.Window(); window = new Timeline.Window();
instanceId = instanceIdCounter++; instanceId = instanceIdCounter++;
@ -1407,6 +1425,10 @@ public class PlayerNotificationManager {
setLargeIcon(builder, largeIcon); setLargeIcon(builder, largeIcon);
builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player)); builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player));
if (groupKey != null) {
builder.setGroup(groupKey);
}
return builder; return builder;
} }
@ -1437,10 +1459,13 @@ public class PlayerNotificationManager {
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && !player.isPlayingAd()) { if (!timeline.isEmpty() && !player.isPlayingAd()) {
timeline.getWindow(player.getCurrentWindowIndex(), window); timeline.getWindow(player.getCurrentWindowIndex(), window);
enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious(); boolean isSeekable = window.isSeekable;
enableRewind = controlDispatcher.isRewindEnabled(); enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
enableFastForward = controlDispatcher.isFastForwardEnabled(); enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
enableNext = window.isDynamic || player.hasNext(); enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
enableNext =
(window.isLive() && window.isDynamic)
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
} }
List<String> stringActions = new ArrayList<>(); 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.Cue;
import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.spherical.SingleTapListener; import com.google.android.exoplayer2.ui.spherical.SingleTapListener;
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
@ -1332,14 +1333,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
closeShutter(); closeShutter();
} }
TrackSelectionArray selections = player.getCurrentTrackSelections(); if (TrackSelectionUtil.hasTrackOfType(player.getCurrentTrackSelections(), C.TRACK_TYPE_VIDEO)) {
for (int i = 0; i < selections.length; i++) { // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { // onRenderedFirstFrame().
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in hideArtwork();
// onRenderedFirstFrame(). return;
hideArtwork();
return;
}
} }
// Video disabled so the shutter must be closed. // Video disabled so the shutter must be closed.

View File

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

View File

@ -607,7 +607,7 @@ import java.util.List;
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true); defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true);
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false); 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(); defaultTimeBar.showScrubber();
} }
} }

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