Commit d80d5485 by Ian Baker Committed by GitHub

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

parents 8ac74a00 6be3a593
Showing with 1483 additions and 717 deletions
...@@ -47,6 +47,7 @@ bazel-testlogs ...@@ -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
......
...@@ -2,22 +2,29 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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)
......
...@@ -13,8 +13,8 @@ ...@@ -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.
......
...@@ -38,6 +38,7 @@ android { ...@@ -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
......
...@@ -31,7 +31,8 @@ ...@@ -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"/>
......
...@@ -34,6 +34,7 @@ android { ...@@ -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
} }
} }
......
...@@ -38,6 +38,7 @@ android { ...@@ -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
......
...@@ -41,7 +41,8 @@ ...@@ -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 @@ ...@@ -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"/>
......
...@@ -34,6 +34,7 @@ android { ...@@ -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
} }
} }
......
...@@ -21,7 +21,8 @@ ...@@ -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"/>
......
...@@ -485,26 +485,6 @@ public final class CastPlayer extends BasePlayer { ...@@ -485,26 +485,6 @@ public final class CastPlayer extends BasePlayer {
} }
@Override @Override
public int getRendererCount() {
// We assume there are three renderers: video, audio, and text.
return RENDERER_COUNT;
}
@Override
public int getRendererType(int index) {
switch (index) {
case RENDERER_INDEX_VIDEO:
return C.TRACK_TYPE_VIDEO;
case RENDERER_INDEX_AUDIO:
return C.TRACK_TYPE_AUDIO;
case RENDERER_INDEX_TEXT:
return C.TRACK_TYPE_TEXT;
default:
throw new IndexOutOfBoundsException();
}
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) { public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
return; return;
...@@ -708,15 +688,19 @@ public final class CastPlayer extends BasePlayer { ...@@ -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);
});
} }
} }
......
...@@ -321,7 +321,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) { 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.
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(); // 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;
} }
...@@ -886,12 +861,48 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -886,12 +861,48 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} }
/** /**
* 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 { ...@@ -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 { ...@@ -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);
} }
} }
......
...@@ -256,6 +256,7 @@ public final class CronetDataSourceTest { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
......
...@@ -30,7 +30,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ...@@ -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>"
......
...@@ -29,7 +29,7 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" ...@@ -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>"
......
...@@ -60,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; ...@@ -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; ...@@ -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) {
......
...@@ -13,8 +13,6 @@ ...@@ -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
......
...@@ -14,7 +14,7 @@ ...@@ -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
......
...@@ -23,15 +23,13 @@ import android.support.v4.media.session.MediaSessionCompat; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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;
} }
} }
...@@ -98,8 +98,10 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu ...@@ -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;
......
...@@ -168,8 +168,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
/** /**
......
...@@ -29,7 +29,7 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" ...@@ -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>"
......
...@@ -29,7 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ...@@ -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>"
......
...@@ -30,44 +30,44 @@ public abstract class BasePlayer implements Player { ...@@ -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);
} }
...@@ -138,6 +138,11 @@ public abstract class BasePlayer implements Player { ...@@ -138,6 +138,11 @@ public abstract class BasePlayer implements Player {
} }
@Override @Override
public final void setPlaybackSpeed(float speed) {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
}
@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 { ...@@ -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;
} }
......
...@@ -79,11 +79,12 @@ public class DefaultControlDispatcher implements ControlDispatcher { ...@@ -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 { ...@@ -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;
......
...@@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -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 { ...@@ -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.
......
...@@ -25,6 +25,7 @@ import androidx.annotation.Nullable; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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,
......
...@@ -19,6 +19,7 @@ import android.os.Bundle; ...@@ -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 { ...@@ -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;
......
...@@ -22,6 +22,7 @@ import androidx.annotation.RequiresApi; ...@@ -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 { ...@@ -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 {}
......
...@@ -85,6 +85,7 @@ public final class DeviceInfo implements Bundleable { ...@@ -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 {}
......
...@@ -41,6 +41,10 @@ public final class DataSourceException extends IOException { ...@@ -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 { ...@@ -56,5 +60,4 @@ public final class DataSourceException extends IOException {
public DataSourceException(int reason) { public DataSourceException(int reason) {
this.reason = reason; this.reason = reason;
} }
} }
...@@ -46,7 +46,6 @@ import java.util.Map; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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;
} }
/** /**
......
...@@ -138,4 +138,11 @@ public final class CopyOnWriteMultiset<E extends Object> implements Iterable<E> ...@@ -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;
}
}
} }
...@@ -107,4 +107,44 @@ public final class CopyOnWriteMultisetTest { ...@@ -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");
}
} }
...@@ -17,15 +17,12 @@ package com.google.android.exoplayer2.upstream; ...@@ -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 { ...@@ -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 { ...@@ -130,5 +97,4 @@ public final class ContentDataSourceTest {
dataSource.close(); dataSource.close();
} }
} }
} }
/*
* 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);
}
}
...@@ -24,7 +24,6 @@ import android.net.Uri; ...@@ -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 ...@@ -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);
} }
......
...@@ -669,6 +669,8 @@ public class DefaultRenderersFactory implements RenderersFactory { ...@@ -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);
} }
} }
...@@ -74,7 +74,8 @@ import java.util.List; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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();
} }
...@@ -999,7 +999,16 @@ import java.util.List; ...@@ -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; ...@@ -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) {
......
...@@ -1980,7 +1980,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -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; ...@@ -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; ...@@ -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)
......
...@@ -53,12 +53,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -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;
......
...@@ -21,6 +21,7 @@ import static java.lang.Math.min; ...@@ -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; ...@@ -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);
} }
} }
......
...@@ -240,7 +240,7 @@ public interface Renderer extends PlayerMessage.Target { ...@@ -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();
......
...@@ -1294,13 +1294,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1294,13 +1294,6 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public void setMediaItems(List<MediaItem> mediaItems) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItems(mediaItems);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) { public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
verifyApplicationThread(); verifyApplicationThread();
analyticsCollector.resetForNewPlaylist(); analyticsCollector.resetForNewPlaylist();
...@@ -1316,27 +1309,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1316,27 +1309,6 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public void setMediaItem(MediaItem mediaItem) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItem(mediaItem);
}
@Override
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItem(mediaItem, resetPosition);
}
@Override
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItem(mediaItem, startPositionMs);
}
@Override
public void setMediaSources(List<MediaSource> mediaSources) { public void setMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread(); verifyApplicationThread();
analyticsCollector.resetForNewPlaylist(); analyticsCollector.resetForNewPlaylist();
...@@ -1392,18 +1364,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1392,18 +1364,6 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public void addMediaItem(MediaItem mediaItem) {
verifyApplicationThread();
player.addMediaItem(mediaItem);
}
@Override
public void addMediaItem(int index, MediaItem mediaItem) {
verifyApplicationThread();
player.addMediaItem(index, mediaItem);
}
@Override
public void addMediaSource(MediaSource mediaSource) { public void addMediaSource(MediaSource mediaSource) {
verifyApplicationThread(); verifyApplicationThread();
player.addMediaSource(mediaSource); player.addMediaSource(mediaSource);
...@@ -1428,24 +1388,12 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1428,24 +1388,12 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public void moveMediaItem(int currentIndex, int newIndex) {
verifyApplicationThread();
player.moveMediaItem(currentIndex, newIndex);
}
@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 @Override
public void removeMediaItem(int index) {
verifyApplicationThread();
player.removeMediaItem(index);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) { public void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThread(); verifyApplicationThread();
player.removeMediaItems(fromIndex, toIndex); player.removeMediaItems(fromIndex, toIndex);
...@@ -2072,8 +2020,8 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -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();
......
...@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; ...@@ -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; ...@@ -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 { ...@@ -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,9 +754,19 @@ public interface AnalyticsListener { ...@@ -755,9 +754,19 @@ 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( default void onAudioDecoderInitialized(
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {}
/** @deprecated Use {@link #onAudioDecoderInitialized(EventTime, String, long, long)}. */
@Deprecated
default void onAudioDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {} EventTime eventTime, String decoderName, long initializationDurationMs) {}
/** /**
...@@ -775,11 +784,10 @@ public interface AnalyticsListener { ...@@ -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,9 +906,19 @@ public interface AnalyticsListener { ...@@ -898,9 +906,19 @@ 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( default void onVideoDecoderInitialized(
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {}
/** @deprecated Use {@link #onVideoDecoderInitialized(EventTime, String, long, long)}. */
@Deprecated
default void onVideoDecoderInitialized(
EventTime eventTime, String decoderName, long initializationDurationMs) {} EventTime eventTime, String decoderName, long initializationDurationMs) {}
/** /**
...@@ -918,11 +936,10 @@ public interface AnalyticsListener { ...@@ -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 { ...@@ -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 { ...@@ -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.
......
...@@ -69,11 +69,8 @@ public interface AudioRendererEventListener { ...@@ -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 { ...@@ -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);
});
} }
} }
......
...@@ -428,7 +428,7 @@ public interface AudioSink { ...@@ -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);
......
...@@ -215,6 +215,35 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) {
......
...@@ -293,11 +293,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -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; ...@@ -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; ...@@ -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) {
......
...@@ -28,13 +28,19 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -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 { ...@@ -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);
});
} }
} }
......
...@@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format; ...@@ -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() {
...@@ -82,6 +99,51 @@ public interface DrmSessionManager { ...@@ -82,6 +99,51 @@ public interface DrmSessionManager {
} }
/** /**
* 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
* not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs * not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs
......
...@@ -733,11 +733,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -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
...@@ -1693,6 +1689,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1693,6 +1689,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* 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.
* *
* @throws ExoPlaybackException If an error occurs releasing or initializing a codec.
* @return False if codec release and re-initialization was triggered. True in all other cases.
*/
protected final boolean updateCodecOperatingRate() throws ExoPlaybackException {
return updateCodecOperatingRate(codecInputFormat);
}
/**
* Updates the codec operating rate, or triggers codec release and re-initialization if a
* previously set operating rate needs to be cleared.
*
* @param format The {@link Format} for which the operating rate should be configured. * @param format The {@link Format} for which the operating rate should be configured.
* @throws ExoPlaybackException If an error occurs releasing or initializing a codec. * @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. * @return False if codec release and re-initialization was triggered. True in all other cases.
...@@ -1702,6 +1709,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -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) {
......
...@@ -19,6 +19,7 @@ import android.os.Handler; ...@@ -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 { ...@@ -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);
} }
} }
......
...@@ -336,7 +336,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource<Void> { ...@@ -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);
......
...@@ -26,7 +26,14 @@ import java.util.List; ...@@ -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.
......
...@@ -31,7 +31,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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 =
......
...@@ -54,7 +54,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource ...@@ -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;
...@@ -73,14 +73,25 @@ public final class ProgressiveMediaSource extends BaseMediaSource ...@@ -73,14 +73,25 @@ public final class ProgressiveMediaSource extends BaseMediaSource
} }
/** /**
* 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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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,
......
...@@ -29,8 +29,12 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; ...@@ -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; ...@@ -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;
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ChunkIndex; ...@@ -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; ...@@ -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 {
......
...@@ -26,6 +26,7 @@ import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.P ...@@ -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; ...@@ -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;
......
...@@ -318,8 +318,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { ...@@ -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(
......
...@@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util; ...@@ -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;
} }
......
...@@ -125,11 +125,21 @@ import java.util.regex.Pattern; ...@@ -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;
......
...@@ -19,6 +19,7 @@ import androidx.annotation.Nullable; ...@@ -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 { ...@@ -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;
}
} }
...@@ -71,7 +71,7 @@ public final class AssetDataSource extends BaseDataSource { ...@@ -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;
......
...@@ -47,13 +47,13 @@ public final class ByteArrayDataSource extends BaseDataSource { ...@@ -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;
......
...@@ -80,7 +80,7 @@ public final class ContentDataSource extends BaseDataSource { ...@@ -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 { ...@@ -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);
} }
} }
} }
......
...@@ -69,7 +69,7 @@ public final class DataSchemeDataSource extends BaseDataSource { ...@@ -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);
} }
......
...@@ -23,7 +23,6 @@ import android.text.TextUtils; ...@@ -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 { ...@@ -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);
......
...@@ -60,7 +60,7 @@ public final class RawResourceDataSource extends BaseDataSource { ...@@ -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 { ...@@ -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 { ...@@ -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
......
...@@ -55,7 +55,6 @@ public final class CacheWriter { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) {
......
...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.analytics.AnalyticsListener; ...@@ -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 { ...@@ -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
......
...@@ -69,11 +69,8 @@ public interface VideoRendererEventListener { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
});
} }
} }
......
...@@ -8606,6 +8606,20 @@ public final class ExoPlayerTest { ...@@ -8606,6 +8606,20 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void playerIdle_withSetPlaybackSpeed_usesPlaybackParameterSpeedWithPitchUnchanged() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1, /* pitch= */ 2));
Player.EventListener mockListener = mock(Player.EventListener.class);
player.addListener(mockListener);
player.prepare();
player.setPlaybackSpeed(2);
verify(mockListener)
.onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2, /* pitch= */ 2));
}
@Test
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed() public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
throws Exception { throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L; long windowStartUnixTimeMs = 987_654_321_000L;
......
...@@ -80,6 +80,7 @@ import com.google.android.exoplayer2.Timeline.Window; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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));
} }
......
...@@ -66,7 +66,7 @@ public final class DefaultAudioSinkTest { ...@@ -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
......
...@@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -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; ...@@ -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 { ...@@ -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();
......
...@@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider; ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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;
......
...@@ -24,22 +24,37 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; ...@@ -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 { ...@@ -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),
......
...@@ -323,17 +323,18 @@ public final class SsaDecoderTest { ...@@ -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);
} }
......
...@@ -87,14 +87,6 @@ public final class ByteArrayDataSourceTest { ...@@ -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.
* *
......
...@@ -50,7 +50,6 @@ public class CacheDataSourceContractTest extends DataSourceContractTest { ...@@ -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 { ...@@ -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);
} }
......
...@@ -39,12 +39,12 @@ public class DataSchemeDataSourceContractTest extends DataSourceContractTest { ...@@ -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());
} }
......
...@@ -108,18 +108,6 @@ public final class DataSchemeDataSourceTest { ...@@ -108,18 +108,6 @@ public final class DataSchemeDataSourceTest {
} }
@Test @Test
public void rangeExceedingResourceLengthRequest() throws IOException {
try {
// Try to open a range exceeding the resource's length.
schemeDataDataSource.open(
buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11));
fail();
} catch (DataSourceException e) {
assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE);
}
}
@Test
public void incorrectScheme() { public void incorrectScheme() {
try { try {
schemeDataDataSource.open(buildDataSpec("http://www.google.com")); schemeDataDataSource.open(buildDataSpec("http://www.google.com"));
......
/*
* 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;
}
}
...@@ -54,13 +54,17 @@ public class UdpDataSourceContractTest extends DataSourceContractTest { ...@@ -54,13 +54,17 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
} }
@Override @Override
protected boolean unboundedReadsAreIndefinite() {
return true;
}
@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 { ...@@ -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.
......
...@@ -17,84 +17,40 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -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 { ...@@ -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);
...@@ -264,6 +221,50 @@ public final class CacheWriterTest { ...@@ -264,6 +221,50 @@ public final class CacheWriterTest {
} }
@Test @Test
public void cache_afterFailureOnClose_succeeds() throws Exception {
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
FakeDataSource upstreamDataSource = new FakeDataSource(fakeDataSet);
AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true);
FailOnCloseDataSink dataSink = new FailOnCloseDataSink(cache, failOnClose);
CacheDataSource cacheDataSource =
new CacheDataSource(
cache,
upstreamDataSource,
new FileDataSource(),
dataSink,
/* flags= */ 0,
/* eventListener= */ null);
CachingCounters counters = new CachingCounters();
CacheWriter cacheWriter =
new CacheWriter(
cacheDataSource,
new DataSpec(Uri.parse("test_data")),
/* allowShortContent= */ false,
/* temporaryBuffer= */ null,
counters);
// DataSink.close failing must cause the operation to fail rather than succeed.
assertThrows(IOException.class, cacheWriter::cache);
// Since all of the bytes were read through the DataSource chain successfully before the sink
// was closed, the progress listener will have seen all of the bytes being cached, even though
// this may not really be the case.
counters.assertValues(
/* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100);
failOnClose.set(false);
// The bytes will be downloaded again, but cached successfully this time.
cacheWriter.cache();
counters.assertValues(
/* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100);
assertCachedData(cache, fakeDataSet);
}
@Test
public void cachePolling() throws Exception { public void cachePolling() throws Exception {
final CachingCounters counters = new CachingCounters(); final CachingCounters counters = new CachingCounters();
FakeDataSet fakeDataSet = FakeDataSet fakeDataSet =
......
...@@ -26,11 +26,6 @@ import com.google.android.exoplayer2.C; ...@@ -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; ...@@ -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 { ...@@ -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());
} }
} }
...@@ -666,26 +664,6 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -666,26 +664,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
/* package */ RepresentationHolder( /* package */ RepresentationHolder(
long periodDurationUs, long periodDurationUs,
int trackType,
Representation representation,
boolean enableEventMessageTrack,
List<Format> closedCaptionFormats,
@Nullable TrackOutput playerEmsgTrackOutput) {
this(
periodDurationUs,
representation,
createChunkExtractor(
trackType,
representation,
enableEventMessageTrack,
closedCaptionFormats,
playerEmsgTrackOutput),
/* segmentNumShift= */ 0,
representation.getIndex());
}
private RepresentationHolder(
long periodDurationUs,
Representation representation, Representation representation,
@Nullable ChunkExtractor chunkExtractor, @Nullable ChunkExtractor chunkExtractor,
long segmentNumShift, long segmentNumShift,
...@@ -800,40 +778,5 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -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);
}
} }
} }
...@@ -913,11 +913,12 @@ public class PlayerControlView extends FrameLayout { ...@@ -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);
} }
} }
......
...@@ -320,6 +320,7 @@ public class PlayerNotificationManager { ...@@ -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 { ...@@ -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 { ...@@ -538,7 +551,8 @@ public class PlayerNotificationManager {
rewindActionIconResourceId, rewindActionIconResourceId,
fastForwardActionIconResourceId, fastForwardActionIconResourceId,
previousActionIconResourceId, previousActionIconResourceId,
nextActionIconResourceId); nextActionIconResourceId,
groupKey);
} }
} }
...@@ -662,6 +676,7 @@ public class PlayerNotificationManager { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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<>();
......
...@@ -58,6 +58,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader; ...@@ -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 ...@@ -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.
......
...@@ -607,7 +607,7 @@ import java.util.List; ...@@ -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();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment