mirror of https://github.com/androidx/media
Merge branch 'dev-v2' into dev-v2-8435-bolditalic
This commit is contained in:
commit
d80d548503
|
@ -47,6 +47,7 @@ bazel-testlogs
|
|||
.DS_Store
|
||||
cmake-build-debug
|
||||
dist
|
||||
jacoco.exec
|
||||
tmp
|
||||
|
||||
# External native builds
|
||||
|
|
|
@ -2,22 +2,29 @@
|
|||
|
||||
### dev-v2 (not yet released)
|
||||
|
||||
* Extractors:
|
||||
* Add support for MP4 and QuickTime meta atoms that are not full atoms.
|
||||
* UI:
|
||||
* Add builder for `PlayerNotificationManager`.
|
||||
* Add group setting to `PlayerNotificationManager`.
|
||||
* Audio:
|
||||
* Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases
|
||||
([#8585](https://github.com/google/ExoPlayer/issues/8585)).
|
||||
* Report unexpected discontinuities in
|
||||
`AnalyticsListener.onAudioSinkError`
|
||||
([#6384](https://github.com/google/ExoPlayer/issues/6384)).
|
||||
* Allow forcing offload for gapless content even if gapless playback is
|
||||
not supported.
|
||||
* Allow fall back from DTS-HD to DTS when playing via passthrough.
|
||||
* Analytics:
|
||||
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
|
||||
* Downloads and caching:
|
||||
* Fix `CacheWriter` to correctly handle `DataSource.close` failures, for
|
||||
which it cannot be assumed that data was successfully written to the
|
||||
cache.
|
||||
* Library restructuring:
|
||||
* `DebugTextViewHelper` moved from `ui` package to `util` package.
|
||||
* Spherical UI components moved from `video.spherical` package to
|
||||
`ui.spherical` package, and made package private.
|
||||
* Core
|
||||
* Move `getRendererCount` and `getRendererType` methods from `Player` to
|
||||
`ExoPlayer`.
|
||||
* Remove deprecated symbols:
|
||||
* Remove `Player.DefaultEventListener`. Use `Player.EventListener`
|
||||
instead.
|
||||
|
@ -25,6 +32,33 @@
|
|||
instead.
|
||||
* Remove `extension-jobdispatcher` module. Use the `extension-workmanager`
|
||||
module instead.
|
||||
* DRM:
|
||||
* Only dispatch DRM session acquire and release events once per period
|
||||
when playing content that uses the same encryption keys for both audio &
|
||||
video tracks (previously separate acquire and release events were
|
||||
dispatched for each track in each period).
|
||||
* Include the session state in DRM session-acquired listener methods.
|
||||
* UI
|
||||
* Fix `StyledPlayerView` scrubber not reappearing correctly in some cases
|
||||
([#8646](https://github.com/google/ExoPlayer/issues/8646)).
|
||||
* MediaSession extension: Remove dependency to core module and rely on common
|
||||
only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for
|
||||
this purpose and does not rely on the `ConcatenatingMediaSource` anymore.
|
||||
|
||||
### 2.13.2 (2021-02-25)
|
||||
|
||||
* Extractors:
|
||||
* Add support for MP4 and QuickTime meta atoms that are not full atoms.
|
||||
* UI:
|
||||
* Make conditions to enable UI actions consistent in
|
||||
`DefaultControlDispatcher`, `PlayerControlView`,
|
||||
`StyledPlayerControlView`, `PlayerNotificationManager` and
|
||||
`TimelineQueueNavigator`.
|
||||
* Fix conditions to enable seeking to next/previous media item to handle
|
||||
the case where a live stream has ended.
|
||||
* Audio:
|
||||
* Fix `SimpleExoPlayer` reporting audio session ID as 0 in some cases
|
||||
([#8585](https://github.com/google/ExoPlayer/issues/8585)).
|
||||
* IMA extension:
|
||||
* Fix a bug where playback could get stuck when seeking into a playlist
|
||||
item with ads, if the preroll ad had preloaded but the window position
|
||||
|
@ -32,13 +66,16 @@
|
|||
* Fix a bug with playback of ads in playlists, where the incorrect period
|
||||
index was used when deciding whether to trigger playback of an ad after
|
||||
a seek.
|
||||
* VP9 extension: Update to use NDK r22
|
||||
* Text:
|
||||
* Parse SSA/ASS font size in `Style:` lines
|
||||
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
|
||||
* VP9 extension: Update to use NDK r21
|
||||
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
|
||||
* FLAC extension: Update to use NDK r22
|
||||
* FLAC extension: Update to use NDK r21
|
||||
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
|
||||
* Opus extension: Update to use NDK r22
|
||||
* Opus extension: Update to use NDK r21
|
||||
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
|
||||
* FFmpeg extension: Update to use NDK r22
|
||||
* FFmpeg extension: Update to use NDK r21
|
||||
([#8581](https://github.com/google/ExoPlayer/issues/8581)).
|
||||
|
||||
### 2.13.1 (2021-02-12)
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.13.1'
|
||||
releaseVersionCode = 2013001
|
||||
releaseVersion = '2.13.2'
|
||||
releaseVersionCode = 2013002
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||
|
|
|
@ -38,6 +38,7 @@ android {
|
|||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTop" android:label="@string/application_name"
|
||||
android:theme="@style/Theme.AppCompat">
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
|
|
@ -34,6 +34,7 @@ android {
|
|||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ android {
|
|||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/Theme.AppCompat">
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
@ -65,7 +66,8 @@
|
|||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTop"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/PlayerTheme">
|
||||
android:theme="@style/PlayerTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.demo.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
|
|
@ -34,6 +34,7 @@ android {
|
|||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name">
|
||||
android:label="@string/application_name"
|
||||
android:exported="true">
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
|
|
@ -484,26 +484,6 @@ public final class CastPlayer extends BasePlayer {
|
|||
sessionManager.endCurrentSession(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererCount() {
|
||||
// We assume there are three renderers: video, audio, and text.
|
||||
return RENDERER_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererType(int index) {
|
||||
switch (index) {
|
||||
case RENDERER_INDEX_VIDEO:
|
||||
return C.TRACK_TYPE_VIDEO;
|
||||
case RENDERER_INDEX_AUDIO:
|
||||
return C.TRACK_TYPE_AUDIO;
|
||||
case RENDERER_INDEX_TEXT:
|
||||
return C.TRACK_TYPE_TEXT;
|
||||
default:
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient == null) {
|
||||
|
@ -708,15 +688,19 @@ public final class CastPlayer extends BasePlayer {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
private void updateTimelineAndNotifyIfChanged() {
|
||||
if (updateTimeline()) {
|
||||
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
|
||||
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
|
||||
Timeline timeline = currentTimeline;
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_TIMELINE_CHANGED,
|
||||
listener ->
|
||||
listener.onTimelineChanged(
|
||||
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
|
||||
listener -> {
|
||||
listener.onTimelineChanged(
|
||||
timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
||||
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -321,7 +321,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
|
||||
// Accessed by the calling thread only.
|
||||
private boolean opened;
|
||||
private long bytesToSkip;
|
||||
private long bytesRemaining;
|
||||
|
||||
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
|
||||
|
@ -577,7 +576,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
byte[] responseBody;
|
||||
try {
|
||||
responseBody = readResponseBody();
|
||||
} catch (HttpDataSourceException e) {
|
||||
} catch (IOException e) {
|
||||
responseBody = Util.EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
|
||||
|
@ -607,7 +606,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||
// requested position.
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
|
||||
// Calculate the content length.
|
||||
if (!isCompressed(responseInfo)) {
|
||||
|
@ -627,6 +626,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
|
||||
try {
|
||||
if (!skipFully(bytesToSkip)) {
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new OpenException(e, dataSpec, Status.READING_RESPONSE);
|
||||
}
|
||||
|
||||
return bytesRemaining;
|
||||
}
|
||||
|
||||
|
@ -641,25 +648,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
if (!readBuffer.hasRemaining()) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
readInternal(readBuffer);
|
||||
try {
|
||||
readInternal(readBuffer);
|
||||
} catch (IOException e) {
|
||||
throw new HttpDataSourceException(
|
||||
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
readBuffer.flip();
|
||||
Assertions.checkState(readBuffer.hasRemaining());
|
||||
if (bytesToSkip > 0) {
|
||||
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
|
||||
readBuffer.position(readBuffer.position() + bytesSkipped);
|
||||
bytesToSkip -= bytesSkipped;
|
||||
}
|
||||
}
|
||||
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
readBuffer.flip();
|
||||
Assertions.checkState(readBuffer.hasRemaining());
|
||||
}
|
||||
|
||||
// Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
|
||||
|
@ -718,17 +725,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
int readLength = buffer.remaining();
|
||||
|
||||
if (readBuffer != null) {
|
||||
// Skip all the bytes we can from readBuffer if there are still bytes to skip.
|
||||
if (bytesToSkip != 0) {
|
||||
if (bytesToSkip >= readBuffer.remaining()) {
|
||||
bytesToSkip -= readBuffer.remaining();
|
||||
readBuffer.position(readBuffer.limit());
|
||||
} else {
|
||||
readBuffer.position(readBuffer.position() + (int) bytesToSkip);
|
||||
bytesToSkip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is existing data in the readBuffer, read as much as possible. Return if any read.
|
||||
int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
|
||||
if (copyBytes != 0) {
|
||||
|
@ -740,44 +736,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
boolean readMore = true;
|
||||
while (readMore) {
|
||||
// If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
|
||||
// buffer. If we do not need to skip bytes, we may write to buffer directly.
|
||||
final boolean useCallerBuffer = bytesToSkip == 0;
|
||||
|
||||
operation.close();
|
||||
|
||||
if (!useCallerBuffer) {
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
readBuffer.clear();
|
||||
if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
|
||||
readBuffer.limit((int) bytesToSkip);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill buffer with more data from Cronet.
|
||||
readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
|
||||
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
Assertions.checkState(
|
||||
useCallerBuffer
|
||||
? readLength > buffer.remaining()
|
||||
: castNonNull(readBuffer).position() > 0);
|
||||
// If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
|
||||
if (useCallerBuffer) {
|
||||
readMore = false;
|
||||
} else {
|
||||
bytesToSkip -= castNonNull(readBuffer).position();
|
||||
}
|
||||
}
|
||||
// Fill buffer with more data from Cronet.
|
||||
operation.close();
|
||||
try {
|
||||
readInternal(buffer);
|
||||
} catch (IOException e) {
|
||||
throw new HttpDataSourceException(
|
||||
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||
}
|
||||
|
||||
final int bytesRead = readLength - buffer.remaining();
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
Assertions.checkState(readLength > buffer.remaining());
|
||||
int bytesRead = readLength - buffer.remaining();
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
|
@ -885,13 +860,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to skip the specified number of bytes in full.
|
||||
*
|
||||
* @param bytesToSkip The number of bytes to skip.
|
||||
* @throws InterruptedIOException If the thread is interrupted during the operation.
|
||||
* @throws IOException If an error occurs reading from the source.
|
||||
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
|
||||
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
|
||||
*/
|
||||
private boolean skipFully(long bytesToSkip) throws IOException {
|
||||
if (bytesToSkip == 0) {
|
||||
return true;
|
||||
}
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (bytesToSkip > 0) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
readInternal(readBuffer);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
if (finished) {
|
||||
return false;
|
||||
} else {
|
||||
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||
readBuffer.flip();
|
||||
Assertions.checkState(readBuffer.hasRemaining());
|
||||
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
|
||||
readBuffer.position(readBuffer.position() + bytesSkipped);
|
||||
bytesToSkip -= bytesSkipped;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the whole response body.
|
||||
*
|
||||
* @return The response body.
|
||||
* @throws HttpDataSourceException If an error occurs reading from the source.
|
||||
* @throws IOException If an error occurs reading from the source.
|
||||
*/
|
||||
private byte[] readResponseBody() throws HttpDataSourceException {
|
||||
private byte[] readResponseBody() throws IOException {
|
||||
byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (!finished) {
|
||||
|
@ -914,10 +925,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
* the current {@code readBuffer} object so that it is not reused in the future.
|
||||
*
|
||||
* @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
|
||||
* @throws HttpDataSourceException If an error occurs reading from the source.
|
||||
* @throws IOException If an error occurs reading from the source.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
|
||||
private void readInternal(ByteBuffer buffer) throws IOException {
|
||||
castNonNull(currentUrlRequest).read(buffer);
|
||||
try {
|
||||
if (!operation.block(readTimeoutMs)) {
|
||||
|
@ -930,23 +941,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||
readBuffer = null;
|
||||
}
|
||||
Thread.currentThread().interrupt();
|
||||
throw new HttpDataSourceException(
|
||||
new InterruptedIOException(),
|
||||
castNonNull(currentDataSpec),
|
||||
HttpDataSourceException.TYPE_READ);
|
||||
throw new InterruptedIOException();
|
||||
} catch (SocketTimeoutException e) {
|
||||
// The operation is ongoing so replace buffer to avoid it being written to by this
|
||||
// operation during a subsequent request.
|
||||
if (buffer == readBuffer) {
|
||||
readBuffer = null;
|
||||
}
|
||||
throw new HttpDataSourceException(
|
||||
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw new HttpDataSourceException(
|
||||
exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -256,6 +256,7 @@ public final class CronetDataSourceTest {
|
|||
public void requestSetsRangeHeader() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 1000);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
// The header value to add is current position to current position + length - 1.
|
||||
|
@ -287,8 +288,6 @@ public final class CronetDataSourceTest {
|
|||
testDataSpec =
|
||||
new DataSpec.Builder()
|
||||
.setUri(TEST_URL)
|
||||
.setPosition(1000)
|
||||
.setLength(5000)
|
||||
.setHttpRequestHeaders(dataSpecRequestProperties)
|
||||
.build();
|
||||
mockResponseStartSuccess();
|
||||
|
@ -1198,6 +1197,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
|
||||
mockSingleRedirectSuccess();
|
||||
mockReadSuccess(0, 1000);
|
||||
|
||||
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
|
||||
|
||||
|
@ -1368,7 +1368,7 @@ public final class CronetDataSourceTest {
|
|||
|
||||
@Test
|
||||
public void allowDirectExecutor() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL));
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
|
|
@ -30,7 +30,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
|
|||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in a shell variable.
|
||||
This build configuration has been tested on NDK r22.
|
||||
This build configuration has been tested on NDK r21.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
|
|
|
@ -29,7 +29,7 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
|
|||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
This build configuration has been tested on NDK r22.
|
||||
This build configuration has been tested on NDK r21.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
|
|
|
@ -60,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener;
|
|||
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
@ -700,12 +701,7 @@ import java.util.Map;
|
|||
|
||||
// Check for a selected track using an audio renderer.
|
||||
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
|
||||
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
|
||||
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return TrackSelectionUtil.hasTrackOfType(trackSelections, C.TRACK_TYPE_AUDIO) ? 100 : 0;
|
||||
}
|
||||
|
||||
private void handleAdEvent(AdEvent adEvent) {
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
// limitations under the License.
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android.defaultConfig.minSdkVersion 19
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-common')
|
||||
implementation 'androidx.collection:collection:' + androidxCollectionVersion
|
||||
|
|
|
@ -28,8 +28,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
|
@ -43,7 +41,6 @@ import androidx.test.core.app.ApplicationProvider;
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.filters.SdkSuppress;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import com.google.android.exoplayer2.ControlDispatcher;
|
||||
|
@ -93,7 +90,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_onceWithAudioResource_changesPlayerStateToPlaying() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
|
||||
|
@ -120,7 +116,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_onceWithAudioResourceOnMainThread_notifiesOnPlayerStateChanged()
|
||||
throws Exception {
|
||||
CountDownLatch onPlayerStatePlayingLatch = new CountDownLatch(1);
|
||||
|
@ -158,7 +153,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_withCustomControlDispatcher_isSkipped() throws Exception {
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
|
@ -194,7 +188,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setMediaItem_withAudioResource_notifiesOnPlaybackCompleted() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
|
||||
|
@ -219,7 +212,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setMediaItem_withVideoResource_notifiesOnPlaybackCompleted() throws Exception {
|
||||
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
|
||||
CountDownLatch onPlaybackCompletedLatch = new CountDownLatch(1);
|
||||
|
@ -243,7 +235,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getDuration_whenIdleState_returnsUnknownTime() {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
|
||||
assertThat(sessionPlayerConnector.getDuration()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
|
||||
|
@ -251,7 +242,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@MediumTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getDuration_afterPrepared_returnsDuration() throws Exception {
|
||||
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
|
||||
|
||||
|
@ -263,7 +253,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getCurrentPosition_whenIdleState_returnsDefaultPosition() {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
|
||||
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
|
||||
|
@ -271,7 +260,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getBufferedPosition_whenIdleState_returnsDefaultPosition() {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
|
||||
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0);
|
||||
|
@ -279,7 +267,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getPlaybackSpeed_whenIdleState_throwsNoException() {
|
||||
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
|
||||
try {
|
||||
|
@ -291,7 +278,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_withDataSourceCallback_changesPlayerState() throws Exception {
|
||||
sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem(R.raw.video_big_buck_bunny));
|
||||
sessionPlayerConnector.prepare();
|
||||
|
@ -308,7 +294,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setMediaItem_withNullMediaItem_throwsException() {
|
||||
try {
|
||||
sessionPlayerConnector.setMediaItem(null);
|
||||
|
@ -320,7 +305,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaybackSpeed_afterPlayback_remainsSame() throws Exception {
|
||||
int resId1 = R.raw.video_big_buck_bunny;
|
||||
MediaItem mediaItem1 =
|
||||
|
@ -363,7 +347,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void seekTo_withSeriesOfSeek_succeeds() throws Exception {
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
|
||||
|
@ -378,7 +361,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void seekTo_skipsUnnecessarySeek() throws Exception {
|
||||
CountDownLatch readAllowedLatch = new CountDownLatch(1);
|
||||
playerTestRule.setDataSourceInstrumentation(
|
||||
|
@ -435,7 +417,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void seekTo_whenUnderlyingPlayerAlsoSeeks_throwsNoException() throws Exception {
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
@ -456,7 +437,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void seekTo_byUnderlyingPlayer_notifiesOnSeekCompleted() throws Exception {
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
@ -484,7 +464,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void getPlayerState_withCallingPrepareAndPlayAndPause_reflectsPlayerState()
|
||||
throws Throwable {
|
||||
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
|
||||
|
@ -521,7 +500,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = VERSION_CODES.KITKAT)
|
||||
public void prepare_twice_finishes() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
@ -530,7 +508,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void prepare_notifiesOnPlayerStateChanged() throws Throwable {
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
|
||||
|
@ -552,7 +529,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void prepare_notifiesBufferingCompletedOnce() throws Throwable {
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
|
||||
|
@ -587,7 +563,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void seekTo_whenPrepared_notifiesOnSeekCompleted() throws Throwable {
|
||||
long mp4DurationMs = 8_484L;
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
|
@ -611,7 +586,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaybackSpeed_whenPrepared_notifiesOnPlaybackSpeedChanged() throws Throwable {
|
||||
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
|
||||
|
||||
|
@ -636,7 +610,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaybackSpeed_withZeroSpeed_throwsException() {
|
||||
try {
|
||||
sessionPlayerConnector.setPlaybackSpeed(0.0f);
|
||||
|
@ -648,7 +621,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaybackSpeed_withNegativeSpeed_throwsException() {
|
||||
try {
|
||||
sessionPlayerConnector.setPlaybackSpeed(-1.0f);
|
||||
|
@ -660,7 +632,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void close_throwsNoExceptionAndDoesNotCrash() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
AudioAttributesCompat attributes =
|
||||
|
@ -679,7 +650,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void cancelReturnedFuture_withSeekTo_cancelsPendingCommand() throws Exception {
|
||||
CountDownLatch readRequestedLatch = new CountDownLatch(1);
|
||||
CountDownLatch readAllowedLatch = new CountDownLatch(1);
|
||||
|
@ -719,7 +689,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_withNullPlaylist_throwsException() throws Exception {
|
||||
try {
|
||||
sessionPlayerConnector.setPlaylist(null, null);
|
||||
|
@ -731,7 +700,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@SmallTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_withPlaylistContainingNullItem_throwsException() {
|
||||
try {
|
||||
List<MediaItem> list = new ArrayList<>();
|
||||
|
@ -745,7 +713,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
|
||||
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
||||
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
|
||||
|
@ -760,7 +727,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylistAndRemoveAllPlaylistItem_playerStateBecomesIdle() throws Exception {
|
||||
List<MediaItem> playlist = new ArrayList<>();
|
||||
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
|
||||
|
@ -786,7 +752,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
|
||||
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
||||
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
|
||||
|
@ -811,7 +776,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged()
|
||||
throws Exception {
|
||||
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
|
||||
|
@ -842,7 +806,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged()
|
||||
throws Exception {
|
||||
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
|
||||
|
@ -876,7 +839,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void addPlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
|
||||
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
|
||||
|
@ -905,7 +867,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void removePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
|
||||
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
|
||||
|
@ -933,7 +894,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void movePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
|
||||
List<MediaItem> playlist = new ArrayList<>();
|
||||
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
|
||||
|
@ -967,7 +927,6 @@ public class SessionPlayerConnectorTest {
|
|||
@Ignore
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void replacePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
|
||||
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
|
||||
|
@ -996,7 +955,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setPlaylist_withPlaylist_notifiesOnCurrentMediaItemChanged() throws Exception {
|
||||
int listSize = 2;
|
||||
List<MediaItem> playlist = TestUtils.createPlaylist(listSize);
|
||||
|
@ -1011,7 +969,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_twice_finishes() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
@ -1021,7 +978,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_withPlaylist_notifiesOnCurrentMediaItemChangedAndOnPlaybackCompleted()
|
||||
throws Exception {
|
||||
List<MediaItem> playlist = new ArrayList<>();
|
||||
|
@ -1060,7 +1016,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void play_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
|
||||
|
@ -1086,7 +1041,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void pause_twice_finishes() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||
|
@ -1097,7 +1051,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void pause_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
|
||||
|
@ -1124,7 +1077,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void pause_byUnderlyingPlayerInListener_changesToPlayerStatePaused() throws Exception {
|
||||
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
|
||||
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
|
||||
|
@ -1169,7 +1121,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void skipToNextAndPrevious_calledInARow_notifiesOnCurrentMediaItemChanged()
|
||||
throws Exception {
|
||||
List<MediaItem> playlist = new ArrayList<>();
|
||||
|
@ -1221,7 +1172,6 @@ public class SessionPlayerConnectorTest {
|
|||
|
||||
@Test
|
||||
@LargeTest
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||
public void setRepeatMode_withRepeatAll_continuesToPlayPlaylistWithoutBeingCompleted()
|
||||
throws Exception {
|
||||
List<MediaItem> playlist = new ArrayList<>();
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-common')
|
||||
api 'androidx.media:media:' + androidxMediaVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
|
|
|
@ -23,15 +23,13 @@ import android.support.v4.media.session.MediaSessionCompat;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ControlDispatcher;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
|
||||
* ConcatenatingMediaSource}.
|
||||
* A {@link MediaSessionConnector.QueueEditor} implementation.
|
||||
*
|
||||
* <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
|
||||
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
|
||||
|
@ -44,18 +42,17 @@ public final class TimelineQueueEditor
|
|||
public static final String EXTRA_FROM_INDEX = "from_index";
|
||||
public static final String EXTRA_TO_INDEX = "to_index";
|
||||
|
||||
/**
|
||||
* Factory to create {@link MediaSource}s.
|
||||
*/
|
||||
public interface MediaSourceFactory {
|
||||
/** Converts a {@link MediaDescriptionCompat} to a {@link MediaItem}. */
|
||||
public interface MediaDescriptionConverter {
|
||||
/**
|
||||
* Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}.
|
||||
* Returns a {@link MediaItem} for the given {@link MediaDescriptionCompat} or null if the
|
||||
* description can't be converted.
|
||||
*
|
||||
* @param description The {@link MediaDescriptionCompat} to create a media source for.
|
||||
* @return A {@link MediaSource} or {@code null} if no source can be created for the given
|
||||
* description.
|
||||
* <p>If not null, the media item that is returned will be used to call {@link
|
||||
* Player#addMediaItem(MediaItem)}.
|
||||
*/
|
||||
@Nullable MediaSource createMediaSource(MediaDescriptionCompat description);
|
||||
@Nullable
|
||||
MediaItem convert(MediaDescriptionCompat description);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,51 +107,46 @@ public final class TimelineQueueEditor
|
|||
public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {
|
||||
return Util.areEqual(d1.getMediaId(), d2.getMediaId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final MediaControllerCompat mediaController;
|
||||
private final QueueDataAdapter queueDataAdapter;
|
||||
private final MediaSourceFactory sourceFactory;
|
||||
private final MediaDescriptionConverter mediaDescriptionConverter;
|
||||
private final MediaDescriptionEqualityChecker equalityChecker;
|
||||
private final ConcatenatingMediaSource queueMediaSource;
|
||||
|
||||
/**
|
||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||
*
|
||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||
* @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media
|
||||
* descriptions to {@link MediaItem MediaItems}.
|
||||
*/
|
||||
public TimelineQueueEditor(
|
||||
MediaControllerCompat mediaController,
|
||||
ConcatenatingMediaSource queueMediaSource,
|
||||
QueueDataAdapter queueDataAdapter,
|
||||
MediaSourceFactory sourceFactory) {
|
||||
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
|
||||
new MediaIdEqualityChecker());
|
||||
MediaDescriptionConverter mediaDescriptionConverter) {
|
||||
this(
|
||||
mediaController, queueDataAdapter, mediaDescriptionConverter, new MediaIdEqualityChecker());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
|
||||
*
|
||||
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
|
||||
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
|
||||
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
|
||||
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
|
||||
* @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media
|
||||
* descriptions to {@link MediaItem MediaItems}.
|
||||
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
|
||||
*/
|
||||
public TimelineQueueEditor(
|
||||
MediaControllerCompat mediaController,
|
||||
ConcatenatingMediaSource queueMediaSource,
|
||||
QueueDataAdapter queueDataAdapter,
|
||||
MediaSourceFactory sourceFactory,
|
||||
MediaDescriptionConverter mediaDescriptionConverter,
|
||||
MediaDescriptionEqualityChecker equalityChecker) {
|
||||
this.mediaController = mediaController;
|
||||
this.queueMediaSource = queueMediaSource;
|
||||
this.queueDataAdapter = queueDataAdapter;
|
||||
this.sourceFactory = sourceFactory;
|
||||
this.mediaDescriptionConverter = mediaDescriptionConverter;
|
||||
this.equalityChecker = equalityChecker;
|
||||
}
|
||||
|
||||
|
@ -165,10 +157,10 @@ public final class TimelineQueueEditor
|
|||
|
||||
@Override
|
||||
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
|
||||
@Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
|
||||
if (mediaSource != null) {
|
||||
@Nullable MediaItem mediaItem = mediaDescriptionConverter.convert(description);
|
||||
if (mediaItem != null) {
|
||||
queueDataAdapter.add(index, description);
|
||||
queueMediaSource.addMediaSource(index, mediaSource);
|
||||
player.addMediaItem(index, mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,7 +170,7 @@ public final class TimelineQueueEditor
|
|||
for (int i = 0; i < queue.size(); i++) {
|
||||
if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
|
||||
queueDataAdapter.remove(i);
|
||||
queueMediaSource.removeMediaSource(i);
|
||||
player.removeMediaItem(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -200,9 +192,8 @@ public final class TimelineQueueEditor
|
|||
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
|
||||
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
|
||||
queueDataAdapter.move(from, to);
|
||||
queueMediaSource.moveMediaSource(from, to);
|
||||
player.moveMediaItem(from, to);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -98,8 +98,10 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||
if (!timeline.isEmpty() && !player.isPlayingAd()) {
|
||||
timeline.getWindow(player.getCurrentWindowIndex(), window);
|
||||
enableSkipTo = timeline.getWindowCount() > 1;
|
||||
enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();
|
||||
enableNext = window.isDynamic || player.hasNext();
|
||||
enablePrevious = window.isSeekable || !window.isLive() || player.hasPrevious();
|
||||
enableNext =
|
||||
(window.isLive() && window.isDynamic)
|
||||
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
|
||||
}
|
||||
|
||||
long actions = 0;
|
||||
|
|
|
@ -168,8 +168,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
private static final byte[] SKIP_BUFFER = new byte[4096];
|
||||
|
||||
private final Call.Factory callFactory;
|
||||
private final RequestProperties requestProperties;
|
||||
|
||||
|
@ -183,10 +181,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
@Nullable private InputStream responseByteStream;
|
||||
private boolean opened;
|
||||
|
||||
private long bytesToSkip;
|
||||
private long bytesToRead;
|
||||
|
||||
private long bytesSkipped;
|
||||
private long bytesToRead;
|
||||
private long bytesRead;
|
||||
|
||||
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
|
||||
|
@ -332,7 +328,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||
// requested position.
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
|
||||
// Determine the length of the data to be read, after skipping.
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
|
@ -345,13 +341,21 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
|
||||
try {
|
||||
if (!skipFully(bytesToSkip)) {
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
|
||||
return bytesToRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
|
||||
try {
|
||||
skipInternal();
|
||||
return readInternal(buffer, offset, readLength);
|
||||
} catch (IOException e) {
|
||||
throw new HttpDataSourceException(
|
||||
|
@ -369,8 +373,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes that have been skipped since the most recent call to
|
||||
* {@link #open(DataSpec)}.
|
||||
* Returns the number of bytes that were skipped during the most recent call to {@link
|
||||
* #open(DataSpec)}.
|
||||
*
|
||||
* @return The number of bytes skipped.
|
||||
*/
|
||||
|
@ -454,30 +458,32 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||
}
|
||||
|
||||
/**
|
||||
* Skips any bytes that need skipping. Else does nothing.
|
||||
* <p>
|
||||
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
|
||||
* Attempts to skip the specified number of bytes in full.
|
||||
*
|
||||
* @param bytesToSkip The number of bytes to skip.
|
||||
* @throws InterruptedIOException If the thread is interrupted during the operation.
|
||||
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
|
||||
* @throws IOException If an error occurs reading from the source.
|
||||
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
|
||||
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
|
||||
*/
|
||||
private void skipInternal() throws IOException {
|
||||
if (bytesSkipped == bytesToSkip) {
|
||||
return;
|
||||
private boolean skipFully(long bytesToSkip) throws IOException {
|
||||
if (bytesToSkip == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] skipBuffer = new byte[4096];
|
||||
while (bytesSkipped != bytesToSkip) {
|
||||
int readLength = (int) min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
|
||||
int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
|
||||
int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
|
||||
int read = castNonNull(responseByteStream).read(skipBuffer, 0, readLength);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
if (read == -1) {
|
||||
throw new EOFException();
|
||||
return false;
|
||||
}
|
||||
bytesSkipped += read;
|
||||
bytesTransferred(read);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,7 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
|
|||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
This build configuration has been tested on NDK r22.
|
||||
This build configuration has been tested on NDK r21.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
|
|
|
@ -29,7 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
|||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
This build configuration has been tested on NDK r22.
|
||||
This build configuration has been tested on NDK r21.
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
|
|
|
@ -30,44 +30,44 @@ public abstract class BasePlayer implements Player {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItem(MediaItem mediaItem) {
|
||||
public final void setMediaItem(MediaItem mediaItem) {
|
||||
setMediaItems(Collections.singletonList(mediaItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
|
||||
public final void setMediaItem(MediaItem mediaItem, long startPositionMs) {
|
||||
setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
|
||||
public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
|
||||
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItems(List<MediaItem> mediaItems) {
|
||||
public final void setMediaItems(List<MediaItem> mediaItems) {
|
||||
setMediaItems(mediaItems, /* resetPosition= */ true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItem(int index, MediaItem mediaItem) {
|
||||
public final void addMediaItem(int index, MediaItem mediaItem) {
|
||||
addMediaItems(index, Collections.singletonList(mediaItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItem(MediaItem mediaItem) {
|
||||
public final void addMediaItem(MediaItem mediaItem) {
|
||||
addMediaItems(Collections.singletonList(mediaItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveMediaItem(int currentIndex, int newIndex) {
|
||||
public final void moveMediaItem(int currentIndex, int newIndex) {
|
||||
if (currentIndex != newIndex) {
|
||||
moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMediaItem(int index) {
|
||||
public final void removeMediaItem(int index) {
|
||||
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,11 @@ public abstract class BasePlayer implements Player {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setPlaybackSpeed(float speed) {
|
||||
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void stop() {
|
||||
stop(/* reset= */ false);
|
||||
|
@ -188,12 +193,12 @@ public abstract class BasePlayer implements Player {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getMediaItemCount() {
|
||||
public final int getMediaItemCount() {
|
||||
return getCurrentTimeline().getWindowCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaItem getMediaItemAt(int index) {
|
||||
public final MediaItem getMediaItemAt(int index) {
|
||||
return getCurrentTimeline().getWindow(index, window).mediaItem;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,11 +79,12 @@ public class DefaultControlDispatcher implements ControlDispatcher {
|
|||
int windowIndex = player.getCurrentWindowIndex();
|
||||
timeline.getWindow(windowIndex, window);
|
||||
int previousWindowIndex = player.getPreviousWindowIndex();
|
||||
boolean isUnseekableLiveStream = window.isLive() && !window.isSeekable;
|
||||
if (previousWindowIndex != C.INDEX_UNSET
|
||||
&& (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|
||||
|| (window.isDynamic && !window.isSeekable))) {
|
||||
|| isUnseekableLiveStream)) {
|
||||
player.seekTo(previousWindowIndex, C.TIME_UNSET);
|
||||
} else {
|
||||
} else if (!isUnseekableLiveStream) {
|
||||
player.seekTo(windowIndex, /* positionMs= */ 0);
|
||||
}
|
||||
return true;
|
||||
|
@ -96,10 +97,11 @@ public class DefaultControlDispatcher implements ControlDispatcher {
|
|||
return true;
|
||||
}
|
||||
int windowIndex = player.getCurrentWindowIndex();
|
||||
timeline.getWindow(windowIndex, window);
|
||||
int nextWindowIndex = player.getNextWindowIndex();
|
||||
if (nextWindowIndex != C.INDEX_UNSET) {
|
||||
player.seekTo(nextWindowIndex, C.TIME_UNSET);
|
||||
} else if (timeline.getWindow(windowIndex, window).isLive()) {
|
||||
} else if (window.isLive() && window.isDynamic) {
|
||||
player.seekTo(windowIndex, C.TIME_UNSET);
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
|
|||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.13.1";
|
||||
public static final String VERSION = "2.13.2";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.1";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
|
@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
|
|||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2013001;
|
||||
public static final int VERSION_INT = 2013002;
|
||||
|
||||
/**
|
||||
* The default user agent for requests made by the library.
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
|
|||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
|
@ -937,6 +938,7 @@ public final class MediaItem implements Bundleable {
|
|||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
FIELD_TARGET_OFFSET_MS,
|
||||
|
@ -1148,6 +1150,7 @@ public final class MediaItem implements Bundleable {
|
|||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
FIELD_START_POSITION_MS,
|
||||
|
@ -1254,6 +1257,7 @@ public final class MediaItem implements Bundleable {
|
|||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
FIELD_MEDIA_ID,
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.os.Bundle;
|
|||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
|
@ -69,10 +70,9 @@ public final class MediaMetadata implements Bundleable {
|
|||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
FIELD_TITLE,
|
||||
})
|
||||
@IntDef({FIELD_TITLE})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
private static final int FIELD_TITLE = 0;
|
||||
|
|
|
@ -55,11 +55,8 @@ import java.util.List;
|
|||
* which can be obtained by calling {@link #getCurrentTimeline()}.
|
||||
* <li>They can provide a {@link TrackGroupArray} defining the currently available tracks, which
|
||||
* can be obtained by calling {@link #getCurrentTrackGroups()}.
|
||||
* <li>They contain a number of renderers, each of which is able to render tracks of a single type
|
||||
* (e.g. audio, video or text). The number of renderers and their respective track types can
|
||||
* be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
|
||||
* <li>They can provide a {@link TrackSelectionArray} defining which of the currently available
|
||||
* tracks are selected to be rendered by each renderer. This can be obtained by calling {@link
|
||||
* tracks are selected to be rendered. This can be obtained by calling {@link
|
||||
* #getCurrentTrackSelections()}}.
|
||||
* </ul>
|
||||
*/
|
||||
|
@ -130,13 +127,17 @@ public interface Player {
|
|||
void clearAuxEffectInfo();
|
||||
|
||||
/**
|
||||
* Sets the audio volume, with 0 being silence and 1 being unity gain.
|
||||
* Sets the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
|
||||
*
|
||||
* @param audioVolume The audio volume.
|
||||
* @param audioVolume Linear output gain to apply to all audio channels.
|
||||
*/
|
||||
void setVolume(float audioVolume);
|
||||
|
||||
/** Returns the audio volume, with 0 being silence and 1 being unity gain. */
|
||||
/**
|
||||
* Returns the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
|
||||
*
|
||||
* @return The linear gain applied to all audio channels.
|
||||
*/
|
||||
float getVolume();
|
||||
|
||||
/**
|
||||
|
@ -400,30 +401,9 @@ public interface Player {
|
|||
* @param timeline The latest timeline. Never null, but may be empty.
|
||||
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
Object manifest = null;
|
||||
if (timeline.getWindowCount() == 1) {
|
||||
// Legacy behavior was to report the manifest for single window timelines only.
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
manifest = timeline.getWindow(0, window).manifest;
|
||||
}
|
||||
// Call deprecated version.
|
||||
onTimelineChanged(timeline, manifest, reason);
|
||||
}
|
||||
default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {}
|
||||
|
||||
/**
|
||||
* Called when the timeline and/or manifest has been refreshed.
|
||||
*
|
||||
* <p>Note that if the timeline has changed then a position discontinuity may also have
|
||||
* occurred. For example, the current period index may have changed as a result of periods being
|
||||
* added or removed from the timeline. This will <em>not</em> be reported via a separate call to
|
||||
* {@link #onPositionDiscontinuity(int)}.
|
||||
*
|
||||
* @param timeline The latest timeline. Never null, but may be empty.
|
||||
* @param manifest The latest manifest in case the timeline has a single window only. Always
|
||||
* null if the timeline has more than a single window.
|
||||
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
|
||||
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
|
||||
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
|
||||
* window).manifest} for a given window index.
|
||||
|
@ -455,8 +435,10 @@ public interface Player {
|
|||
* other events that happen in the same {@link Looper} message queue iteration.
|
||||
*
|
||||
* @param trackGroups The available tracks. Never null, but may be of length zero.
|
||||
* @param trackSelections The track selections for each renderer. Never null and always of
|
||||
* length {@link #getRendererCount()}, but may contain null elements.
|
||||
* @param trackSelections The selected tracks. Never null, but may contain null elements. A
|
||||
* concrete implementation may include null elements if it has a fixed number of renderer
|
||||
* components, wishes to report a TrackSelection for each of them, and has one or more
|
||||
* renderer components that is not assigned any selected tracks.
|
||||
*/
|
||||
default void onTracksChanged(
|
||||
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
|
||||
|
@ -488,10 +470,7 @@ public interface Player {
|
|||
*
|
||||
* @param isLoading Whether the source is currently being loaded.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onIsLoadingChanged(boolean isLoading) {
|
||||
onLoadingChanged(isLoading);
|
||||
}
|
||||
default void onIsLoadingChanged(boolean isLoading) {}
|
||||
|
||||
/** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */
|
||||
@Deprecated
|
||||
|
@ -1131,6 +1110,7 @@ public interface Player {
|
|||
* Returns the current {@link State playback state} of the player.
|
||||
*
|
||||
* @return The current {@link State playback state}.
|
||||
* @see EventListener#onPlaybackStateChanged(int)
|
||||
*/
|
||||
@State
|
||||
int getPlaybackState();
|
||||
|
@ -1140,6 +1120,7 @@ public interface Player {
|
|||
* true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
|
||||
*
|
||||
* @return The current {@link PlaybackSuppressionReason playback suppression reason}.
|
||||
* @see EventListener#onPlaybackSuppressionReasonChanged(int)
|
||||
*/
|
||||
@PlaybackSuppressionReason
|
||||
int getPlaybackSuppressionReason();
|
||||
|
@ -1156,6 +1137,7 @@ public interface Player {
|
|||
* </ul>
|
||||
*
|
||||
* @return Whether the player is playing.
|
||||
* @see EventListener#onIsPlayingChanged(boolean)
|
||||
*/
|
||||
boolean isPlaying();
|
||||
|
||||
|
@ -1168,6 +1150,7 @@ public interface Player {
|
|||
* {@link #STATE_IDLE}.
|
||||
*
|
||||
* @return The error, or {@code null}.
|
||||
* @see EventListener#onPlayerError(ExoPlaybackException)
|
||||
*/
|
||||
@Nullable
|
||||
ExoPlaybackException getPlayerError();
|
||||
|
@ -1199,6 +1182,7 @@ public interface Player {
|
|||
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
|
||||
*
|
||||
* @return Whether playback will proceed when ready.
|
||||
* @see EventListener#onPlayWhenReadyChanged(boolean, int)
|
||||
*/
|
||||
boolean getPlayWhenReady();
|
||||
|
||||
|
@ -1213,6 +1197,7 @@ public interface Player {
|
|||
* Returns the current {@link RepeatMode} used for playback.
|
||||
*
|
||||
* @return The current repeat mode.
|
||||
* @see EventListener#onRepeatModeChanged(int)
|
||||
*/
|
||||
@RepeatMode
|
||||
int getRepeatMode();
|
||||
|
@ -1224,13 +1209,18 @@ public interface Player {
|
|||
*/
|
||||
void setShuffleModeEnabled(boolean shuffleModeEnabled);
|
||||
|
||||
/** Returns whether shuffling of windows is enabled. */
|
||||
/**
|
||||
* Returns whether shuffling of windows is enabled.
|
||||
*
|
||||
* @see EventListener#onShuffleModeEnabledChanged(boolean)
|
||||
*/
|
||||
boolean getShuffleModeEnabled();
|
||||
|
||||
/**
|
||||
* Whether the player is currently loading the source.
|
||||
*
|
||||
* @return Whether the player is currently loading the source.
|
||||
* @see EventListener#onIsLoadingChanged(boolean)
|
||||
*/
|
||||
boolean isLoading();
|
||||
|
||||
|
@ -1325,6 +1315,18 @@ public interface Player {
|
|||
*/
|
||||
void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
|
||||
|
||||
/**
|
||||
* Changes the rate at which playback occurs.
|
||||
*
|
||||
* <p>The pitch is not changed.
|
||||
*
|
||||
* <p>This is equivalent to {@code setPlaybackParameter(getPlaybackParameter().withSpeed(speed))}.
|
||||
*
|
||||
* @param speed The linear factor by which playback will be sped up. Must be higher than 0. 1 is
|
||||
* normal speed, 2 is twice as fast, 0.5 is half normal speed...
|
||||
*/
|
||||
void setPlaybackSpeed(float speed);
|
||||
|
||||
/**
|
||||
* Returns the currently active playback parameters.
|
||||
*
|
||||
|
@ -1359,24 +1361,22 @@ public interface Player {
|
|||
*/
|
||||
void release();
|
||||
|
||||
/** Returns the number of renderers. */
|
||||
int getRendererCount();
|
||||
|
||||
/**
|
||||
* Returns the track type that the renderer at a given index handles.
|
||||
* Returns the available track groups.
|
||||
*
|
||||
* <p>For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will
|
||||
* return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}.
|
||||
*
|
||||
* @param index The index of the renderer.
|
||||
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
|
||||
* @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
|
||||
*/
|
||||
int getRendererType(int index);
|
||||
|
||||
/** Returns the available track groups. */
|
||||
TrackGroupArray getCurrentTrackGroups();
|
||||
|
||||
/** Returns the current track selections for each renderer. */
|
||||
/**
|
||||
* Returns the current track selections.
|
||||
*
|
||||
* <p>A concrete implementation may include null elements if it has a fixed number of renderer
|
||||
* components, wishes to report a TrackSelection for each of them, and has one or more renderer
|
||||
* components that is not assigned any selected tracks.
|
||||
*
|
||||
* @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
|
||||
*/
|
||||
TrackSelectionArray getCurrentTrackSelections();
|
||||
|
||||
/**
|
||||
|
@ -1389,6 +1389,8 @@ public interface Player {
|
|||
*
|
||||
* <p>This metadata is considered static in that it comes from the tracks' declared Formats,
|
||||
* rather than being timed (or dynamic) metadata, which is represented within a metadata track.
|
||||
*
|
||||
* @see EventListener#onStaticMetadataChanged(List)
|
||||
*/
|
||||
List<Metadata> getCurrentStaticMetadata();
|
||||
|
||||
|
@ -1398,7 +1400,11 @@ public interface Player {
|
|||
@Nullable
|
||||
Object getCurrentManifest();
|
||||
|
||||
/** Returns the current {@link Timeline}. Never null, but may be empty. */
|
||||
/**
|
||||
* Returns the current {@link Timeline}. Never null, but may be empty.
|
||||
*
|
||||
* @see EventListener#onTimelineChanged(Timeline, int)
|
||||
*/
|
||||
Timeline getCurrentTimeline();
|
||||
|
||||
/** Returns the index of the period currently being played. */
|
||||
|
@ -1444,6 +1450,8 @@ public interface Player {
|
|||
/**
|
||||
* Returns the media item of the current window in the timeline. May be null if the timeline is
|
||||
* empty.
|
||||
*
|
||||
* @see EventListener#onMediaItemTransition(MediaItem, int)
|
||||
*/
|
||||
@Nullable
|
||||
MediaItem getCurrentMediaItem();
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.annotation.RequiresApi;
|
|||
import com.google.android.exoplayer2.Bundleable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
|
@ -166,6 +167,7 @@ public final class AudioAttributes implements Bundleable {
|
|||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({FIELD_CONTENT_TYPE, FIELD_FLAGS, FIELD_USAGE, FIELD_ALLOWED_CAPTURE_POLICY})
|
||||
private @interface FieldNumber {}
|
||||
|
|
|
@ -85,6 +85,7 @@ public final class DeviceInfo implements Bundleable {
|
|||
|
||||
// Bundleable implementation.
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({FIELD_PLAYBACK_TYPE, FIELD_MIN_VOLUME, FIELD_MAX_VOLUME})
|
||||
private @interface FieldNumber {}
|
||||
|
|
|
@ -41,6 +41,10 @@ public final class DataSourceException extends IOException {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the {@link DataSpec#position starting position} of the request was outside the
|
||||
* bounds of the data.
|
||||
*/
|
||||
public static final int POSITION_OUT_OF_RANGE = 0;
|
||||
|
||||
/**
|
||||
|
@ -56,5 +60,4 @@ public final class DataSourceException extends IOException {
|
|||
public DataSourceException(int reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ import java.util.Map;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
|
||||
|
@ -221,14 +220,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
@Nullable private DataSpec dataSpec;
|
||||
@Nullable private HttpURLConnection connection;
|
||||
@Nullable private InputStream inputStream;
|
||||
private byte @MonotonicNonNull [] skipBuffer;
|
||||
private boolean opened;
|
||||
private int responseCode;
|
||||
|
||||
private long bytesToSkip;
|
||||
private long bytesToRead;
|
||||
|
||||
private long bytesSkipped;
|
||||
private long bytesToRead;
|
||||
private long bytesRead;
|
||||
|
||||
/** @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */
|
||||
|
@ -400,7 +396,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||
// requested position.
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
|
||||
// Determine the length of the data to be read, after skipping.
|
||||
boolean isCompressed = isCompressed(connection);
|
||||
|
@ -432,13 +428,21 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
|
||||
try {
|
||||
if (!skipFully(bytesToSkip)) {
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
closeConnectionQuietly();
|
||||
throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||
}
|
||||
|
||||
return bytesToRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
|
||||
try {
|
||||
skipInternal();
|
||||
return readInternal(buffer, offset, readLength);
|
||||
} catch (IOException e) {
|
||||
throw new HttpDataSourceException(
|
||||
|
@ -480,8 +484,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes that have been skipped since the most recent call to
|
||||
* {@link #open(DataSpec)}.
|
||||
* Returns the number of bytes that were skipped during the most recent call to {@link
|
||||
* #open(DataSpec)}.
|
||||
*
|
||||
* @return The number of bytes skipped.
|
||||
*/
|
||||
|
@ -725,22 +729,19 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
}
|
||||
|
||||
/**
|
||||
* Skips any bytes that need skipping. Else does nothing.
|
||||
* <p>
|
||||
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
|
||||
* Attempts to skip the specified number of bytes in full.
|
||||
*
|
||||
* @param bytesToSkip The number of bytes to skip.
|
||||
* @throws InterruptedIOException If the thread is interrupted during the operation.
|
||||
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
|
||||
* @throws IOException If an error occurs reading from the source.
|
||||
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
|
||||
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
|
||||
*/
|
||||
private void skipInternal() throws IOException {
|
||||
if (bytesSkipped == bytesToSkip) {
|
||||
return;
|
||||
private boolean skipFully(long bytesToSkip) throws IOException {
|
||||
if (bytesToSkip == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (skipBuffer == null) {
|
||||
skipBuffer = new byte[4096];
|
||||
}
|
||||
|
||||
byte[] skipBuffer = new byte[4096];
|
||||
while (bytesSkipped != bytesToSkip) {
|
||||
int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
|
||||
int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
|
||||
|
@ -748,11 +749,12 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||
throw new InterruptedIOException();
|
||||
}
|
||||
if (read == -1) {
|
||||
throw new EOFException();
|
||||
return false;
|
||||
}
|
||||
bytesSkipped += read;
|
||||
bytesTransferred(read);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -138,4 +138,11 @@ public final class CopyOnWriteMultiset<E extends Object> implements Iterable<E>
|
|||
return elements.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the number of occurrences of an element in this multiset. */
|
||||
public int count(E element) {
|
||||
synchronized (lock) {
|
||||
return elementCounts.containsKey(element) ? elementCounts.get(element) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,4 +107,44 @@ public final class CopyOnWriteMultisetTest {
|
|||
|
||||
assertThrows(UnsupportedOperationException.class, () -> elementSet.remove("a string"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void count() {
|
||||
CopyOnWriteMultiset<String> multiset = new CopyOnWriteMultiset<>();
|
||||
multiset.add("a string");
|
||||
multiset.add("a string");
|
||||
|
||||
assertThat(multiset.count("a string")).isEqualTo(2);
|
||||
assertThat(multiset.count("another string")).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modifyingWhileIteratingElements_succeeds() {
|
||||
CopyOnWriteMultiset<String> multiset = new CopyOnWriteMultiset<>();
|
||||
multiset.add("a string");
|
||||
multiset.add("a string");
|
||||
multiset.add("another string");
|
||||
|
||||
// A traditional collection would throw a ConcurrentModificationException here.
|
||||
for (String element : multiset) {
|
||||
multiset.remove(element);
|
||||
}
|
||||
|
||||
assertThat(multiset).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modifyingWhileIteratingElementSet_succeeds() {
|
||||
CopyOnWriteMultiset<String> multiset = new CopyOnWriteMultiset<>();
|
||||
multiset.add("a string");
|
||||
multiset.add("a string");
|
||||
multiset.add("another string");
|
||||
|
||||
// A traditional collection would throw a ConcurrentModificationException here.
|
||||
for (String element : multiset.elementSet()) {
|
||||
multiset.remove(element);
|
||||
}
|
||||
|
||||
assertThat(multiset).containsExactly("a string");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,12 @@ package com.google.android.exoplayer2.upstream;
|
|||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static junit.framework.Assert.fail;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.ContentDataSource.ContentDataSourceException;
|
||||
import java.io.EOFException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
@ -85,36 +82,6 @@ public final class ContentDataSourceTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void read_positionPastEndOfContent_throwsEOFException() throws Exception {
|
||||
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, /* pipeMode= */ false);
|
||||
ContentDataSource dataSource =
|
||||
new ContentDataSource(ApplicationProvider.getApplicationContext());
|
||||
DataSpec dataSpec = new DataSpec(contentUri, /* position= */ 1025, C.LENGTH_UNSET);
|
||||
try {
|
||||
ContentDataSourceException exception =
|
||||
assertThrows(ContentDataSourceException.class, () -> dataSource.open(dataSpec));
|
||||
assertThat(exception).hasCauseThat().isInstanceOf(EOFException.class);
|
||||
} finally {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readPipeMode_positionPastEndOfContent_throwsEOFException() throws Exception {
|
||||
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, /* pipeMode= */ true);
|
||||
ContentDataSource dataSource =
|
||||
new ContentDataSource(ApplicationProvider.getApplicationContext());
|
||||
DataSpec dataSpec = new DataSpec(contentUri, /* position= */ 1025, C.LENGTH_UNSET);
|
||||
try {
|
||||
ContentDataSourceException exception =
|
||||
assertThrows(ContentDataSourceException.class, () -> dataSource.open(dataSpec));
|
||||
assertThat(exception).hasCauseThat().isInstanceOf(EOFException.class);
|
||||
} finally {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertData(int offset, int length, boolean pipeMode) throws IOException {
|
||||
Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);
|
||||
ContentDataSource dataSource =
|
||||
|
@ -130,5 +97,4 @@ public final class ContentDataSourceTest {
|
|||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.core.test.R;
|
||||
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** {@link DataSource} contract tests for {@link RawResourceDataSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class RawResourceDataSourceContractTest extends DataSourceContractTest {
|
||||
|
||||
private static final byte[] RESOURCE_1_DATA = Util.getUtf8Bytes("resource1 abc\n");
|
||||
private static final byte[] RESOURCE_2_DATA = Util.getUtf8Bytes("resource2 abcdef\n");
|
||||
|
||||
@Override
|
||||
protected DataSource createDataSource() {
|
||||
return new RawResourceDataSource(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImmutableList<TestResource> getTestResources() {
|
||||
// Android packages raw resources into a single file. When reading a resource other than the
|
||||
// last one, Android does not prevent accidentally reading beyond the end of the resource and
|
||||
// into the next one. We use two resources in this test to ensure that when packaged, at least
|
||||
// one of them has a subsequent resource. This allows the contract test to enforce that the
|
||||
// RawResourceDataSource implementation doesn't erroneously read into the second resource when
|
||||
// opened to read the first.
|
||||
return ImmutableList.of(
|
||||
new TestResource.Builder()
|
||||
.setName("resource 1")
|
||||
.setUri(RawResourceDataSource.buildRawResourceUri(R.raw.resource1))
|
||||
.setExpectedBytes(RESOURCE_1_DATA)
|
||||
.build(),
|
||||
new TestResource.Builder()
|
||||
.setName("resource 2")
|
||||
.setUri(RawResourceDataSource.buildRawResourceUri(R.raw.resource2))
|
||||
.setExpectedBytes(RESOURCE_2_DATA)
|
||||
.build(),
|
||||
// Additional resources using different URI schemes.
|
||||
new TestResource.Builder()
|
||||
.setName("android.resource:// with path")
|
||||
.setUri(
|
||||
Uri.parse(
|
||||
"android.resource://"
|
||||
+ ApplicationProvider.getApplicationContext().getPackageName()
|
||||
+ "/raw/resource1"))
|
||||
.setExpectedBytes(RESOURCE_1_DATA)
|
||||
.build(),
|
||||
new TestResource.Builder()
|
||||
.setName("android.resource:// with ID")
|
||||
.setUri(
|
||||
Uri.parse(
|
||||
"android.resource://"
|
||||
+ ApplicationProvider.getApplicationContext().getPackageName()
|
||||
+ "/"
|
||||
+ R.raw.resource1))
|
||||
.setExpectedBytes(RESOURCE_1_DATA)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri getNotFoundUri() {
|
||||
return RawResourceDataSource.buildRawResourceUri(Resources.ID_NULL);
|
||||
}
|
||||
}
|
|
@ -24,7 +24,6 @@ import android.net.Uri;
|
|||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -73,7 +72,7 @@ public final class TestContentProvider extends ContentProvider
|
|||
openPipeHelper(
|
||||
uri, /* mimeType= */ null, /* opts= */ null, /* args= */ null, /* func= */ this);
|
||||
return new AssetFileDescriptor(
|
||||
fileDescriptor, /* startOffset= */ 0, /* length= */ C.LENGTH_UNSET);
|
||||
fileDescriptor, /* startOffset= */ 0, AssetFileDescriptor.UNKNOWN_LENGTH);
|
||||
} else {
|
||||
return getContext().getAssets().openFd(fileName);
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
resource1 abc
|
|
@ -0,0 +1 @@
|
|||
resource2 abcdef
|
|
@ -669,6 +669,8 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||
new DefaultAudioProcessorChain(),
|
||||
enableFloatOutput,
|
||||
enableAudioTrackPlaybackParams,
|
||||
enableOffload);
|
||||
enableOffload
|
||||
? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
|
||||
: DefaultAudioSink.OFFLOAD_MODE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,8 @@ import java.util.List;
|
|||
* provides default implementations for common media types ({@link MediaCodecVideoRenderer},
|
||||
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
|
||||
* Renderer consumes media from the MediaSource being played. Renderers are injected when the
|
||||
* player is created.
|
||||
* player is created. The number of renderers and their respective track types can be obtained
|
||||
* by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
|
||||
* <li>A <b>{@link TrackSelector}</b> that selects tracks provided by the MediaSource to be
|
||||
* consumed by each of the available Renderers. The library provides a default implementation
|
||||
* ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected
|
||||
|
@ -449,6 +450,20 @@ public interface ExoPlayer extends Player {
|
|||
}
|
||||
}
|
||||
|
||||
/** Returns the number of renderers. */
|
||||
int getRendererCount();
|
||||
|
||||
/**
|
||||
* Returns the track type that the renderer at a given index handles.
|
||||
*
|
||||
* <p>For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will
|
||||
* return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}.
|
||||
*
|
||||
* @param index The index of the renderer.
|
||||
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
|
||||
*/
|
||||
int getRendererType(int index);
|
||||
|
||||
/**
|
||||
* Returns the track selector that this player uses, or null if track selection is not supported.
|
||||
*/
|
||||
|
@ -663,7 +678,7 @@ public interface ExoPlayer extends Player {
|
|||
* <li>Audio offload rendering is enabled in {@link
|
||||
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
|
||||
* DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
|
||||
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, boolean)}.
|
||||
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, int)}.
|
||||
* <li>An audio track is playing in a format that the device supports offloading (for example,
|
||||
* MP3 or AAC).
|
||||
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.
|
||||
|
@ -682,6 +697,7 @@ public interface ExoPlayer extends Player {
|
|||
* Returns whether the player has paused its main loop to save power in offload scheduling mode.
|
||||
*
|
||||
* @see #experimentalSetOffloadSchedulingEnabled(boolean)
|
||||
* @see EventListener#onExperimentalSleepingForOffloadChanged(boolean)
|
||||
*/
|
||||
boolean experimentalIsSleepingForOffload();
|
||||
}
|
||||
|
|
|
@ -999,7 +999,16 @@ import java.util.List;
|
|||
if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_TIMELINE_CHANGED,
|
||||
listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason));
|
||||
listener -> {
|
||||
@Nullable Object manifest = null;
|
||||
if (newPlaybackInfo.timeline.getWindowCount() == 1) {
|
||||
// Legacy behavior was to report the manifest for single window timelines only.
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
manifest = newPlaybackInfo.timeline.getWindow(0, window).manifest;
|
||||
}
|
||||
listener.onTimelineChanged(newPlaybackInfo.timeline, manifest, timelineChangeReason);
|
||||
listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason);
|
||||
});
|
||||
}
|
||||
if (positionDiscontinuity) {
|
||||
listeners.queueEvent(
|
||||
|
@ -1042,7 +1051,10 @@ import java.util.List;
|
|||
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
|
||||
listeners.queueEvent(
|
||||
Player.EVENT_IS_LOADING_CHANGED,
|
||||
listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading));
|
||||
listener -> {
|
||||
listener.onLoadingChanged(newPlaybackInfo.isLoading);
|
||||
listener.onIsLoadingChanged(newPlaybackInfo.isLoading);
|
||||
});
|
||||
}
|
||||
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState
|
||||
|| previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {
|
||||
|
|
|
@ -1980,7 +1980,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
@Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod();
|
||||
if (readingPeriod == null
|
||||
|| queue.getPlayingPeriod() == readingPeriod
|
||||
|| readingPeriod.allRenderersEnabled) {
|
||||
|| readingPeriod.allRenderersInCorrectState) {
|
||||
// Not reading ahead or all renderers updated.
|
||||
return;
|
||||
}
|
||||
|
@ -2075,7 +2075,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();
|
||||
return nextPlayingPeriodHolder != null
|
||||
&& rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime()
|
||||
&& nextPlayingPeriodHolder.allRenderersEnabled;
|
||||
&& nextPlayingPeriodHolder.allRenderersInCorrectState;
|
||||
}
|
||||
|
||||
private boolean hasReadingPeriodFinishedReading() {
|
||||
|
@ -2294,7 +2294,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
enableRenderer(i, rendererWasEnabledFlags[i]);
|
||||
}
|
||||
}
|
||||
readingMediaPeriod.allRenderersEnabled = true;
|
||||
readingMediaPeriod.allRenderersInCorrectState = true;
|
||||
}
|
||||
|
||||
private void enableRenderer(int rendererIndex, boolean wasRendererEnabled)
|
||||
|
|
|
@ -53,12 +53,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
/** {@link MediaPeriodInfo} about this media period. */
|
||||
public MediaPeriodInfo info;
|
||||
/**
|
||||
* Whether all required renderers have been enabled with the {@link #sampleStreams} for this
|
||||
* Whether all renderers are in the correct state for this {@link #mediaPeriod}.
|
||||
*
|
||||
* <p>Renderers that are needed must have been enabled with the {@link #sampleStreams} for this
|
||||
* {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[],
|
||||
* SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[],
|
||||
* SampleStream, long)} has been called.
|
||||
* SampleStream, long, boolean, boolean, long, long)} or {@link Renderer#replaceStream(Format[],
|
||||
* SampleStream, long, long)} has been called.
|
||||
*
|
||||
* <p>Renderers that are not needed must have been {@link Renderer#disable() disabled}.
|
||||
*/
|
||||
public boolean allRenderersEnabled;
|
||||
public boolean allRenderersInCorrectState;
|
||||
|
||||
private final boolean[] mayRetainStreamFlags;
|
||||
private final RendererCapabilities[] rendererCapabilities;
|
||||
|
|
|
@ -21,6 +21,7 @@ import static java.lang.Math.min;
|
|||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaPeriod;
|
||||
|
@ -600,9 +601,11 @@ import java.util.Set;
|
|||
|
||||
@Override
|
||||
public void onDrmSessionAcquired(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
@DrmSession.State int state) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmSessionAcquired();
|
||||
drmEventDispatcher.drmSessionAcquired(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -240,7 +240,7 @@ public interface Renderer extends PlayerMessage.Target {
|
|||
/**
|
||||
* Returns the track type that the renderer handles.
|
||||
*
|
||||
* @see Player#getRendererType(int)
|
||||
* @see ExoPlayer#getRendererType(int)
|
||||
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
|
||||
*/
|
||||
int getTrackType();
|
||||
|
|
|
@ -1293,13 +1293,6 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
prepare();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItems(List<MediaItem> mediaItems) {
|
||||
verifyApplicationThread();
|
||||
analyticsCollector.resetForNewPlaylist();
|
||||
player.setMediaItems(mediaItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
|
||||
verifyApplicationThread();
|
||||
|
@ -1315,27 +1308,6 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
player.setMediaItems(mediaItems, startWindowIndex, startPositionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItem(MediaItem mediaItem) {
|
||||
verifyApplicationThread();
|
||||
analyticsCollector.resetForNewPlaylist();
|
||||
player.setMediaItem(mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
|
||||
verifyApplicationThread();
|
||||
analyticsCollector.resetForNewPlaylist();
|
||||
player.setMediaItem(mediaItem, resetPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
|
||||
verifyApplicationThread();
|
||||
analyticsCollector.resetForNewPlaylist();
|
||||
player.setMediaItem(mediaItem, startPositionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaSources(List<MediaSource> mediaSources) {
|
||||
verifyApplicationThread();
|
||||
|
@ -1391,18 +1363,6 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
player.addMediaItems(index, mediaItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItem(MediaItem mediaItem) {
|
||||
verifyApplicationThread();
|
||||
player.addMediaItem(mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItem(int index, MediaItem mediaItem) {
|
||||
verifyApplicationThread();
|
||||
player.addMediaItem(index, mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaSource(MediaSource mediaSource) {
|
||||
verifyApplicationThread();
|
||||
|
@ -1427,24 +1387,12 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
player.addMediaSources(index, mediaSources);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveMediaItem(int currentIndex, int newIndex) {
|
||||
verifyApplicationThread();
|
||||
player.moveMediaItem(currentIndex, newIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
|
||||
verifyApplicationThread();
|
||||
player.moveMediaItems(fromIndex, toIndex, newIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMediaItem(int index) {
|
||||
verifyApplicationThread();
|
||||
player.removeMediaItem(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||
verifyApplicationThread();
|
||||
|
@ -2072,8 +2020,8 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame(Surface surface) {
|
||||
analyticsCollector.onRenderedFirstFrame(surface);
|
||||
public void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {
|
||||
analyticsCollector.onRenderedFirstFrame(surface, renderTimeMs);
|
||||
if (SimpleExoPlayer.this.surface == surface) {
|
||||
for (VideoListener videoListener : videoListeners) {
|
||||
videoListener.onRenderedFirstFrame();
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes;
|
|||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
|
@ -207,7 +208,7 @@ public class AnalyticsCollector
|
|||
|
||||
// AudioRendererEventListener implementation.
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onAudioEnabled(DecoderCounters counters) {
|
||||
EventTime eventTime = generateReadingMediaPeriodEventTime();
|
||||
|
@ -220,7 +221,7 @@ public class AnalyticsCollector
|
|||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onAudioDecoderInitialized(
|
||||
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
|
||||
|
@ -230,12 +231,14 @@ public class AnalyticsCollector
|
|||
AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED,
|
||||
listener -> {
|
||||
listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs);
|
||||
listener.onAudioDecoderInitialized(
|
||||
eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
|
||||
listener.onDecoderInitialized(
|
||||
eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onAudioInputFormatChanged(
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
|
@ -244,6 +247,7 @@ public class AnalyticsCollector
|
|||
eventTime,
|
||||
AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED,
|
||||
listener -> {
|
||||
listener.onAudioInputFormatChanged(eventTime, format);
|
||||
listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation);
|
||||
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format);
|
||||
});
|
||||
|
@ -278,7 +282,7 @@ public class AnalyticsCollector
|
|||
listener -> listener.onAudioDecoderReleased(eventTime, decoderName));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onAudioDisabled(DecoderCounters counters) {
|
||||
EventTime eventTime = generatePlayingMediaPeriodEventTime();
|
||||
|
@ -361,7 +365,7 @@ public class AnalyticsCollector
|
|||
|
||||
// VideoRendererEventListener implementation.
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onVideoEnabled(DecoderCounters counters) {
|
||||
EventTime eventTime = generateReadingMediaPeriodEventTime();
|
||||
|
@ -374,7 +378,7 @@ public class AnalyticsCollector
|
|||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onVideoDecoderInitialized(
|
||||
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
|
||||
|
@ -384,12 +388,14 @@ public class AnalyticsCollector
|
|||
AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED,
|
||||
listener -> {
|
||||
listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs);
|
||||
listener.onVideoDecoderInitialized(
|
||||
eventTime, decoderName, initializedTimestampMs, initializationDurationMs);
|
||||
listener.onDecoderInitialized(
|
||||
eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onVideoInputFormatChanged(
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
|
@ -398,6 +404,7 @@ public class AnalyticsCollector
|
|||
eventTime,
|
||||
AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED,
|
||||
listener -> {
|
||||
listener.onVideoInputFormatChanged(eventTime, format);
|
||||
listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation);
|
||||
listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format);
|
||||
});
|
||||
|
@ -421,7 +428,7 @@ public class AnalyticsCollector
|
|||
listener -> listener.onVideoDecoderReleased(eventTime, decoderName));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onVideoDisabled(DecoderCounters counters) {
|
||||
EventTime eventTime = generatePlayingMediaPeriodEventTime();
|
||||
|
@ -446,13 +453,17 @@ public class AnalyticsCollector
|
|||
eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onRenderedFirstFrame(@Nullable Surface surface) {
|
||||
public final void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {
|
||||
EventTime eventTime = generateReadingMediaPeriodEventTime();
|
||||
sendEvent(
|
||||
eventTime,
|
||||
AnalyticsListener.EVENT_RENDERED_FIRST_FRAME,
|
||||
listener -> listener.onRenderedFirstFrame(eventTime, surface));
|
||||
listener -> {
|
||||
listener.onRenderedFirstFrame(eventTime, surface);
|
||||
listener.onRenderedFirstFrame(eventTime, surface, renderTimeMs);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -615,16 +626,20 @@ public class AnalyticsCollector
|
|||
listener -> listener.onStaticMetadataChanged(eventTime, metadataList));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
@Override
|
||||
public final void onIsLoadingChanged(boolean isLoading) {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
sendEvent(
|
||||
eventTime,
|
||||
AnalyticsListener.EVENT_IS_LOADING_CHANGED,
|
||||
listener -> listener.onIsLoadingChanged(eventTime, isLoading));
|
||||
listener -> {
|
||||
listener.onLoadingChanged(eventTime, isLoading);
|
||||
listener.onIsLoadingChanged(eventTime, isLoading);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
|
||||
@Override
|
||||
public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
|
@ -725,7 +740,7 @@ public class AnalyticsCollector
|
|||
listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
|
||||
@Override
|
||||
public final void onSeekProcessed() {
|
||||
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
|
||||
|
@ -747,12 +762,17 @@ public class AnalyticsCollector
|
|||
// DefaultDrmSessionManager.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public final void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
|
||||
@SuppressWarnings("deprecation") // Calls deprecated listener method.
|
||||
public final void onDrmSessionAcquired(
|
||||
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {
|
||||
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
|
||||
sendEvent(
|
||||
eventTime,
|
||||
AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED,
|
||||
listener -> listener.onDrmSessionAcquired(eventTime));
|
||||
listener -> {
|
||||
listener.onDrmSessionAcquired(eventTime);
|
||||
listener.onDrmSessionAcquired(eventTime, state);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
|||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodec.CodecException;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.IntDef;
|
||||
|
@ -39,6 +40,7 @@ import com.google.android.exoplayer2.audio.AudioSink;
|
|||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderException;
|
||||
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||
|
@ -583,10 +585,7 @@ public interface AnalyticsListener {
|
|||
* @param eventTime The event time.
|
||||
* @param isLoading Whether the player is loading.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {
|
||||
onLoadingChanged(eventTime, isLoading);
|
||||
}
|
||||
default void onIsLoadingChanged(EventTime eventTime, boolean isLoading) {}
|
||||
|
||||
/** @deprecated Use {@link #onIsLoadingChanged(EventTime, boolean)} instead. */
|
||||
@Deprecated
|
||||
|
@ -755,8 +754,18 @@ public interface AnalyticsListener {
|
|||
*
|
||||
* @param eventTime The event time.
|
||||
* @param decoderName The decoder that was created.
|
||||
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
|
||||
* finished.
|
||||
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
|
||||
*/
|
||||
default void onAudioDecoderInitialized(
|
||||
EventTime eventTime,
|
||||
String decoderName,
|
||||
long initializedTimestampMs,
|
||||
long initializationDurationMs) {}
|
||||
|
||||
/** @deprecated Use {@link #onAudioDecoderInitialized(EventTime, String, long, long)}. */
|
||||
@Deprecated
|
||||
default void onAudioDecoderInitialized(
|
||||
EventTime eventTime, String decoderName, long initializationDurationMs) {}
|
||||
|
||||
|
@ -775,11 +784,10 @@ public interface AnalyticsListener {
|
|||
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
|
||||
* have a decoder.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onAudioInputFormatChanged(
|
||||
EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
onAudioInputFormatChanged(eventTime, format);
|
||||
}
|
||||
EventTime eventTime,
|
||||
Format format,
|
||||
@Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
|
||||
|
||||
/**
|
||||
* Called when the audio position has increased for the first time since the last pause or
|
||||
|
@ -898,8 +906,18 @@ public interface AnalyticsListener {
|
|||
*
|
||||
* @param eventTime The event time.
|
||||
* @param decoderName The decoder that was created.
|
||||
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
|
||||
* finished.
|
||||
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
|
||||
*/
|
||||
default void onVideoDecoderInitialized(
|
||||
EventTime eventTime,
|
||||
String decoderName,
|
||||
long initializedTimestampMs,
|
||||
long initializationDurationMs) {}
|
||||
|
||||
/** @deprecated Use {@link #onVideoDecoderInitialized(EventTime, String, long, long)}. */
|
||||
@Deprecated
|
||||
default void onVideoDecoderInitialized(
|
||||
EventTime eventTime, String decoderName, long initializationDurationMs) {}
|
||||
|
||||
|
@ -918,11 +936,10 @@ public interface AnalyticsListener {
|
|||
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
|
||||
* have a decoder.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onVideoInputFormatChanged(
|
||||
EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
onVideoInputFormatChanged(eventTime, format);
|
||||
}
|
||||
EventTime eventTime,
|
||||
Format format,
|
||||
@Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
|
||||
|
||||
/**
|
||||
* Called after video frames have been dropped.
|
||||
|
@ -992,7 +1009,13 @@ public interface AnalyticsListener {
|
|||
* @param eventTime The event time.
|
||||
* @param surface The {@link Surface} to which a frame has been rendered, or {@code null} if the
|
||||
* renderer renders to something that isn't a {@link Surface}.
|
||||
* @param renderTimeMs {@link SystemClock#elapsedRealtime()} when the first frame was rendered.
|
||||
*/
|
||||
default void onRenderedFirstFrame(
|
||||
EventTime eventTime, @Nullable Surface surface, long renderTimeMs) {}
|
||||
|
||||
/** @deprecated Use {@link #onRenderedFirstFrame(EventTime, Surface, long)} instead. */
|
||||
@Deprecated
|
||||
default void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {}
|
||||
|
||||
/**
|
||||
|
@ -1026,12 +1049,17 @@ public interface AnalyticsListener {
|
|||
*/
|
||||
default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}
|
||||
|
||||
/** @deprecated Implement {@link #onDrmSessionAcquired(EventTime, int)} instead. */
|
||||
@Deprecated
|
||||
default void onDrmSessionAcquired(EventTime eventTime) {}
|
||||
|
||||
/**
|
||||
* Called each time a drm session is acquired.
|
||||
*
|
||||
* @param eventTime The event time.
|
||||
* @param state The {@link DrmSession.State} of the session when the acquisition completed.
|
||||
*/
|
||||
default void onDrmSessionAcquired(EventTime eventTime) {}
|
||||
default void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {}
|
||||
|
||||
/**
|
||||
* Called each time drm keys are loaded.
|
||||
|
|
|
@ -69,11 +69,8 @@ public interface AudioRendererEventListener {
|
|||
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
|
||||
* have a decoder.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onAudioInputFormatChanged(
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
onAudioInputFormatChanged(format);
|
||||
}
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
|
||||
|
||||
/**
|
||||
* Called when the audio position has increased for the first time since the last pause or
|
||||
|
@ -186,11 +183,15 @@ public interface AudioRendererEventListener {
|
|||
}
|
||||
|
||||
/** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
public void inputFormatChanged(
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() -> castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation));
|
||||
() -> {
|
||||
castNonNull(listener).onAudioInputFormatChanged(format);
|
||||
castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -428,7 +428,7 @@ public interface AudioSink {
|
|||
/**
|
||||
* Sets the playback volume.
|
||||
*
|
||||
* @param volume A volume in the range [0.0, 1.0].
|
||||
* @param volume Linear output gain to apply to all channels. Should be in the range [0.0, 1.0].
|
||||
*/
|
||||
void setVolume(float volume);
|
||||
|
||||
|
|
|
@ -215,6 +215,35 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
/** The default skip silence flag. */
|
||||
private static final boolean DEFAULT_SKIP_SILENCE = false;
|
||||
|
||||
/** Audio offload mode configuration. */
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
OFFLOAD_MODE_DISABLED,
|
||||
OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED,
|
||||
OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED
|
||||
})
|
||||
public @interface OffloadMode {}
|
||||
|
||||
/** The audio sink will never play in offload mode. */
|
||||
public static final int OFFLOAD_MODE_DISABLED = 0;
|
||||
/**
|
||||
* The audio sink will prefer offload playback except if the track is gapless and the device does
|
||||
* not advertise support for gapless playback in offload.
|
||||
*
|
||||
* <p>Use this option to prioritize seamless transitions between tracks of the same album to power
|
||||
* savings.
|
||||
*/
|
||||
public static final int OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED = 1;
|
||||
/**
|
||||
* The audio sink will prefer offload playback even if this might result in silence gaps between
|
||||
* tracks.
|
||||
*
|
||||
* <p>Use this option to prioritize battery saving at the cost of a possible non seamless
|
||||
* transitions between tracks of the same album.
|
||||
*/
|
||||
public static final int OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED = 2;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH})
|
||||
|
@ -281,7 +310,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
private final AudioTrackPositionTracker audioTrackPositionTracker;
|
||||
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
|
||||
private final boolean enableAudioTrackPlaybackParams;
|
||||
private final boolean enableOffload;
|
||||
@OffloadMode private final int offloadMode;
|
||||
@MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29;
|
||||
private final PendingExceptionHolder<InitializationException>
|
||||
initializationExceptionPendingExceptionHolder;
|
||||
|
@ -364,7 +393,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
new DefaultAudioProcessorChain(audioProcessors),
|
||||
enableFloatOutput,
|
||||
/* enableAudioTrackPlaybackParams= */ false,
|
||||
/* enableOffload= */ false);
|
||||
OFFLOAD_MODE_DISABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -382,8 +411,8 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
* use.
|
||||
* @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
|
||||
* android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
|
||||
* @param enableOffload Whether to enable audio offload. If an audio format can be both played
|
||||
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is
|
||||
* @param offloadMode Audio offload configuration. If an audio format can be both played with
|
||||
* offload and encoded audio passthrough, it will be played in offload. Audio offload is
|
||||
* supported from API level 29. Most Android devices can only support one offload {@link
|
||||
* android.media.AudioTrack} at a time and can invalidate it at any time. Thus an app can
|
||||
* never be guaranteed that it will be able to play in offload. Audio processing (for example,
|
||||
|
@ -394,12 +423,12 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
AudioProcessorChain audioProcessorChain,
|
||||
boolean enableFloatOutput,
|
||||
boolean enableAudioTrackPlaybackParams,
|
||||
boolean enableOffload) {
|
||||
@OffloadMode int offloadMode) {
|
||||
this.audioCapabilities = audioCapabilities;
|
||||
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
|
||||
this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput;
|
||||
this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams;
|
||||
this.enableOffload = Util.SDK_INT >= 29 && enableOffload;
|
||||
this.offloadMode = Util.SDK_INT >= 29 ? offloadMode : OFFLOAD_MODE_DISABLED;
|
||||
releasingConditionVariable = new ConditionVariable(true);
|
||||
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
|
||||
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
|
||||
|
@ -462,9 +491,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
// guaranteed to support.
|
||||
return SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
|
||||
}
|
||||
if (enableOffload
|
||||
&& !offloadDisabledUntilNextConfiguration
|
||||
&& isOffloadedPlaybackSupported(format, audioAttributes)) {
|
||||
if (!offloadDisabledUntilNextConfiguration && useOffloadedPlayback(format, audioAttributes)) {
|
||||
return SINK_FORMAT_SUPPORTED_DIRECTLY;
|
||||
}
|
||||
if (isPassthroughPlaybackSupported(format, audioCapabilities)) {
|
||||
|
@ -541,7 +568,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
availableAudioProcessors = new AudioProcessor[0];
|
||||
outputSampleRate = inputFormat.sampleRate;
|
||||
outputPcmFrameSize = C.LENGTH_UNSET;
|
||||
if (enableOffload && isOffloadedPlaybackSupported(inputFormat, audioAttributes)) {
|
||||
if (useOffloadedPlayback(inputFormat, audioAttributes)) {
|
||||
outputMode = OUTPUT_MODE_OFFLOAD;
|
||||
outputEncoding =
|
||||
MimeTypes.getEncoding(
|
||||
|
@ -1478,6 +1505,10 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
&& !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) {
|
||||
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
|
||||
encoding = C.ENCODING_E_AC3;
|
||||
} else if (encoding == C.ENCODING_DTS_HD
|
||||
&& !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) {
|
||||
// DTS receivers support DTS-HD streams (but decode only the core layer).
|
||||
encoding = C.ENCODING_DTS;
|
||||
}
|
||||
if (!audioCapabilities.supportsEncoding(encoding)) {
|
||||
return null;
|
||||
|
@ -1561,9 +1592,8 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
return Util.getAudioTrackChannelConfig(channelCount);
|
||||
}
|
||||
|
||||
private static boolean isOffloadedPlaybackSupported(
|
||||
Format format, AudioAttributes audioAttributes) {
|
||||
if (Util.SDK_INT < 29) {
|
||||
private boolean useOffloadedPlayback(Format format, AudioAttributes audioAttributes) {
|
||||
if (Util.SDK_INT < 29 || offloadMode == OFFLOAD_MODE_DISABLED) {
|
||||
return false;
|
||||
}
|
||||
@C.Encoding
|
||||
|
@ -1581,8 +1611,12 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
audioFormat, audioAttributes.getAudioAttributesV21())) {
|
||||
return false;
|
||||
}
|
||||
boolean notGapless = format.encoderDelay == 0 && format.encoderPadding == 0;
|
||||
return notGapless || isOffloadedGaplessPlaybackSupported();
|
||||
boolean isGapless = format.encoderDelay != 0 || format.encoderPadding != 0;
|
||||
boolean offloadRequiresGaplessSupport = offloadMode == OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED;
|
||||
if (isGapless && offloadRequiresGaplessSupport && !isOffloadedGaplessPlaybackSupported()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isOffloadedPlayback(AudioTrack audioTrack) {
|
||||
|
|
|
@ -293,11 +293,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
if (openInternal(true)) {
|
||||
doLicense(true);
|
||||
}
|
||||
} else if (eventDispatcher != null && isOpen()) {
|
||||
// If the session is already open then send the acquire event only to the provided dispatcher.
|
||||
// TODO: Add a parameter to onDrmSessionAcquired to indicate whether the session is being
|
||||
// re-used or not.
|
||||
eventDispatcher.drmSessionAcquired();
|
||||
} else if (eventDispatcher != null
|
||||
&& isOpen()
|
||||
&& eventDispatchers.count(eventDispatcher) == 1) {
|
||||
// If the session is already open and this is the first instance of eventDispatcher we've
|
||||
// seen, then send the acquire event only to the provided dispatcher.
|
||||
eventDispatcher.drmSessionAcquired(state);
|
||||
}
|
||||
referenceCountListener.onReferenceCountIncremented(this, referenceCount);
|
||||
}
|
||||
|
@ -321,15 +322,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
mediaDrm.closeSession(sessionId);
|
||||
sessionId = null;
|
||||
}
|
||||
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmSessionReleased);
|
||||
}
|
||||
if (eventDispatcher != null) {
|
||||
if (isOpen()) {
|
||||
// If the session is still open then send the release event only to the provided dispatcher
|
||||
// before removing it.
|
||||
eventDispatchers.remove(eventDispatcher);
|
||||
if (eventDispatchers.count(eventDispatcher) == 0) {
|
||||
// Release events are only sent to the last-attached instance of each EventDispatcher.
|
||||
eventDispatcher.drmSessionReleased();
|
||||
}
|
||||
eventDispatchers.remove(eventDispatcher);
|
||||
}
|
||||
referenceCountListener.onReferenceCountDecremented(this, referenceCount);
|
||||
}
|
||||
|
@ -353,8 +352,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
try {
|
||||
sessionId = mediaDrm.openSession();
|
||||
mediaCrypto = mediaDrm.createMediaCrypto(sessionId);
|
||||
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmSessionAcquired);
|
||||
state = STATE_OPENED;
|
||||
// Capture state into a local so a consistent value is seen by the lambda.
|
||||
int localState = state;
|
||||
dispatchEvent(eventDispatcher -> eventDispatcher.drmSessionAcquired(localState));
|
||||
Assertions.checkNotNull(sessionId);
|
||||
return true;
|
||||
} catch (NotProvisionedException e) {
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.ResourceBusyException;
|
||||
import android.os.Handler;
|
||||
|
@ -31,7 +35,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
|
|||
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
@ -47,9 +50,16 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */
|
||||
/**
|
||||
* A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}.
|
||||
*
|
||||
* <p>This implementation supports pre-acquisition of sessions using {@link
|
||||
* #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
|
||||
*/
|
||||
@RequiresApi(18)
|
||||
public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||
|
||||
|
@ -120,8 +130,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
*/
|
||||
public Builder setUuidAndExoMediaDrmProvider(
|
||||
UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {
|
||||
this.uuid = Assertions.checkNotNull(uuid);
|
||||
this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider);
|
||||
this.uuid = checkNotNull(uuid);
|
||||
this.exoMediaDrmProvider = checkNotNull(exoMediaDrmProvider);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -157,8 +167,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
public Builder setUseDrmSessionsForClearContent(
|
||||
int... useDrmSessionsForClearContentTrackTypes) {
|
||||
for (int trackType : useDrmSessionsForClearContentTrackTypes) {
|
||||
Assertions.checkArgument(
|
||||
trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
|
||||
checkArgument(trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
this.useDrmSessionsForClearContentTrackTypes =
|
||||
useDrmSessionsForClearContentTrackTypes.clone();
|
||||
|
@ -185,7 +194,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
* @return This builder.
|
||||
*/
|
||||
public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||
this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy);
|
||||
this.loadErrorHandlingPolicy = checkNotNull(loadErrorHandlingPolicy);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -205,7 +214,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
* @return This builder.
|
||||
*/
|
||||
public Builder setSessionKeepaliveMs(long sessionKeepaliveMs) {
|
||||
Assertions.checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET);
|
||||
checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET);
|
||||
this.sessionKeepaliveMs = sessionKeepaliveMs;
|
||||
return this;
|
||||
}
|
||||
|
@ -282,14 +291,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
|
||||
private final List<DefaultDrmSession> sessions;
|
||||
private final List<DefaultDrmSession> provisioningSessions;
|
||||
private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
|
||||
private final Set<DefaultDrmSession> keepaliveSessions;
|
||||
|
||||
private int prepareCallsCount;
|
||||
@Nullable private ExoMediaDrm exoMediaDrm;
|
||||
@Nullable private DefaultDrmSession placeholderDrmSession;
|
||||
@Nullable private DefaultDrmSession noMultiSessionDrmSession;
|
||||
@Nullable private Looper playbackLooper;
|
||||
private @MonotonicNonNull Handler sessionReleasingHandler;
|
||||
private @MonotonicNonNull Looper playbackLooper;
|
||||
private @MonotonicNonNull Handler playbackHandler;
|
||||
private int mode;
|
||||
@Nullable private byte[] offlineLicenseKeySetId;
|
||||
|
||||
|
@ -388,8 +398,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
boolean playClearSamplesWithoutKeys,
|
||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||
long sessionKeepaliveMs) {
|
||||
Assertions.checkNotNull(uuid);
|
||||
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
|
||||
checkNotNull(uuid);
|
||||
checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
|
||||
this.uuid = uuid;
|
||||
this.exoMediaDrmProvider = exoMediaDrmProvider;
|
||||
this.callback = callback;
|
||||
|
@ -403,6 +413,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
mode = MODE_PLAYBACK;
|
||||
sessions = new ArrayList<>();
|
||||
provisioningSessions = new ArrayList<>();
|
||||
preacquiredSessionReferences = Sets.newIdentityHashSet();
|
||||
keepaliveSessions = Sets.newIdentityHashSet();
|
||||
this.sessionKeepaliveMs = sessionKeepaliveMs;
|
||||
}
|
||||
|
@ -432,9 +443,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
|
||||
*/
|
||||
public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {
|
||||
Assertions.checkState(sessions.isEmpty());
|
||||
checkState(sessions.isEmpty());
|
||||
if (mode == MODE_QUERY || mode == MODE_RELEASE) {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
checkNotNull(offlineLicenseKeySetId);
|
||||
}
|
||||
this.mode = mode;
|
||||
this.offlineLicenseKeySetId = offlineLicenseKeySetId;
|
||||
|
@ -447,7 +458,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
if (prepareCallsCount++ != 0) {
|
||||
return;
|
||||
}
|
||||
Assertions.checkState(exoMediaDrm == null);
|
||||
checkState(exoMediaDrm == null);
|
||||
exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
|
||||
exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
|
||||
}
|
||||
|
@ -466,10 +477,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
sessions.get(i).release(/* eventDispatcher= */ null);
|
||||
}
|
||||
}
|
||||
Assertions.checkNotNull(exoMediaDrm).release();
|
||||
releaseAllPreacquiredSessions();
|
||||
|
||||
checkNotNull(exoMediaDrm).release();
|
||||
exoMediaDrm = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DrmSessionReference preacquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
initPlaybackLooper(playbackLooper);
|
||||
PreacquiredSessionReference preacquiredSessionReference =
|
||||
new PreacquiredSessionReference(eventDispatcher);
|
||||
preacquiredSessionReference.acquire(format);
|
||||
return preacquiredSessionReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DrmSession acquireSession(
|
||||
|
@ -477,16 +502,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
initPlaybackLooper(playbackLooper);
|
||||
return acquireSession(
|
||||
playbackLooper,
|
||||
eventDispatcher,
|
||||
format,
|
||||
/* shouldReleasePreacquiredSessionsBeforeRetrying= */ true);
|
||||
}
|
||||
|
||||
// Must be called on the playback thread.
|
||||
@Nullable
|
||||
private DrmSession acquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format,
|
||||
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
|
||||
maybeCreateMediaDrmHandler(playbackLooper);
|
||||
|
||||
if (format.drmInitData == null) {
|
||||
// Content is not encrypted.
|
||||
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType));
|
||||
return maybeAcquirePlaceholderSession(
|
||||
MimeTypes.getTrackType(format.sampleMimeType),
|
||||
shouldReleasePreacquiredSessionsBeforeRetrying);
|
||||
}
|
||||
|
||||
@Nullable List<SchemeData> schemeDatas = null;
|
||||
if (offlineLicenseKeySetId == null) {
|
||||
schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false);
|
||||
schemeDatas = getSchemeDatas(checkNotNull(format.drmInitData), uuid, false);
|
||||
if (schemeDatas.isEmpty()) {
|
||||
final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
|
||||
Log.e(TAG, "DRM error", error);
|
||||
|
@ -515,7 +556,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
// Create a new session.
|
||||
session =
|
||||
createAndAcquireSessionWithRetry(
|
||||
schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher);
|
||||
schemeDatas,
|
||||
/* isPlaceholderSession= */ false,
|
||||
eventDispatcher,
|
||||
shouldReleasePreacquiredSessionsBeforeRetrying);
|
||||
if (!multiSession) {
|
||||
noMultiSessionDrmSession = session;
|
||||
}
|
||||
|
@ -531,7 +575,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
@Nullable
|
||||
public Class<? extends ExoMediaCrypto> getExoMediaCryptoType(Format format) {
|
||||
Class<? extends ExoMediaCrypto> exoMediaCryptoType =
|
||||
Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType();
|
||||
checkNotNull(exoMediaDrm).getExoMediaCryptoType();
|
||||
if (format.drmInitData == null) {
|
||||
int trackType = MimeTypes.getTrackType(format.sampleMimeType);
|
||||
return Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) != C.INDEX_UNSET
|
||||
|
@ -547,8 +591,9 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
// Internal methods.
|
||||
|
||||
@Nullable
|
||||
private DrmSession maybeAcquirePlaceholderSession(int trackType) {
|
||||
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
|
||||
private DrmSession maybeAcquirePlaceholderSession(
|
||||
int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
|
||||
ExoMediaDrm exoMediaDrm = checkNotNull(this.exoMediaDrm);
|
||||
boolean avoidPlaceholderDrmSessions =
|
||||
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
|
||||
&& FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
|
||||
|
@ -563,7 +608,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
createAndAcquireSessionWithRetry(
|
||||
/* schemeDatas= */ ImmutableList.of(),
|
||||
/* isPlaceholderSession= */ true,
|
||||
/* eventDispatcher= */ null);
|
||||
/* eventDispatcher= */ null,
|
||||
shouldReleasePreacquiredSessionsBeforeRetrying);
|
||||
sessions.add(placeholderDrmSession);
|
||||
this.placeholderDrmSession = placeholderDrmSession;
|
||||
} else {
|
||||
|
@ -607,12 +653,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
private void initPlaybackLooper(Looper playbackLooper) {
|
||||
@EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"})
|
||||
private synchronized void initPlaybackLooper(Looper playbackLooper) {
|
||||
if (this.playbackLooper == null) {
|
||||
this.playbackLooper = playbackLooper;
|
||||
this.sessionReleasingHandler = new Handler(playbackLooper);
|
||||
this.playbackHandler = new Handler(playbackLooper);
|
||||
} else {
|
||||
Assertions.checkState(this.playbackLooper == playbackLooper);
|
||||
checkState(this.playbackLooper == playbackLooper);
|
||||
checkNotNull(playbackHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -625,35 +673,67 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
private DefaultDrmSession createAndAcquireSessionWithRetry(
|
||||
@Nullable List<SchemeData> schemeDatas,
|
||||
boolean isPlaceholderSession,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
|
||||
DefaultDrmSession session =
|
||||
createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
|
||||
if (session.getState() == DrmSession.STATE_ERROR
|
||||
&& (Util.SDK_INT < 19
|
||||
|| Assertions.checkNotNull(session.getError()).getCause()
|
||||
instanceof ResourceBusyException)) {
|
||||
// We're short on DRM session resources, so eagerly release all our keepalive sessions.
|
||||
// ResourceBusyException is only available at API 19, so on earlier versions we always
|
||||
// eagerly release regardless of the underlying error.
|
||||
if (!keepaliveSessions.isEmpty()) {
|
||||
// Make a local copy, because sessions are removed from this.keepaliveSessions during
|
||||
// release (via callback).
|
||||
ImmutableSet<DefaultDrmSession> keepaliveSessions =
|
||||
ImmutableSet.copyOf(this.keepaliveSessions);
|
||||
for (DrmSession keepaliveSession : keepaliveSessions) {
|
||||
keepaliveSession.release(/* eventDispatcher= */ null);
|
||||
}
|
||||
// Undo the acquisitions from createAndAcquireSession().
|
||||
session.release(eventDispatcher);
|
||||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||
session.release(/* eventDispatcher= */ null);
|
||||
}
|
||||
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
|
||||
// If we're short on DRM session resources, first try eagerly releasing all our keepalive
|
||||
// sessions and then retry the acquisition.
|
||||
if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) {
|
||||
// Make a local copy, because sessions are removed from this.keepaliveSessions during
|
||||
// release (via callback).
|
||||
ImmutableSet<DefaultDrmSession> keepaliveSessions =
|
||||
ImmutableSet.copyOf(this.keepaliveSessions);
|
||||
for (DrmSession keepaliveSession : keepaliveSessions) {
|
||||
keepaliveSession.release(/* eventDispatcher= */ null);
|
||||
}
|
||||
undoAcquisition(session, eventDispatcher);
|
||||
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
|
||||
}
|
||||
|
||||
// If the acquisition failed again due to continued resource shortage, and
|
||||
// shouldReleasePreacquiredSessionsBeforeRetrying is true, try releasing all pre-acquired
|
||||
// sessions and then retry the acquisition.
|
||||
if (acquisitionFailedIndicatingResourceShortage(session)
|
||||
&& shouldReleasePreacquiredSessionsBeforeRetrying
|
||||
&& !preacquiredSessionReferences.isEmpty()) {
|
||||
releaseAllPreacquiredSessions();
|
||||
undoAcquisition(session, eventDispatcher);
|
||||
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) {
|
||||
// ResourceBusyException is only available at API 19, so on earlier versions we
|
||||
// assume any error indicates resource shortage (ensuring we retry).
|
||||
return session.getState() == DrmSession.STATE_ERROR
|
||||
&& (Util.SDK_INT < 19
|
||||
|| checkNotNull(session.getError()).getCause() instanceof ResourceBusyException);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the acquisitions from {@link #createAndAcquireSession(List, boolean,
|
||||
* DrmSessionEventListener.EventDispatcher)}.
|
||||
*/
|
||||
private void undoAcquisition(
|
||||
DrmSession session, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
|
||||
session.release(eventDispatcher);
|
||||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||
session.release(/* eventDispatcher= */ null);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseAllPreacquiredSessions() {
|
||||
// Make a local copy, because sessions are removed from this.preacquiredSessionReferences
|
||||
// during release (via callback).
|
||||
ImmutableSet<PreacquiredSessionReference> preacquiredSessionReferences =
|
||||
ImmutableSet.copyOf(this.preacquiredSessionReferences);
|
||||
for (PreacquiredSessionReference preacquiredSessionReference : preacquiredSessionReferences) {
|
||||
preacquiredSessionReference.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in
|
||||
* {@code eventDispatcher}).
|
||||
|
@ -665,7 +745,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
@Nullable List<SchemeData> schemeDatas,
|
||||
boolean isPlaceholderSession,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
|
||||
Assertions.checkNotNull(exoMediaDrm);
|
||||
checkNotNull(exoMediaDrm);
|
||||
// Placeholder sessions should always play clear samples without keys.
|
||||
boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;
|
||||
DefaultDrmSession session =
|
||||
|
@ -681,7 +761,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
offlineLicenseKeySetId,
|
||||
keyRequestParameters,
|
||||
callback,
|
||||
Assertions.checkNotNull(playbackLooper),
|
||||
checkNotNull(playbackLooper),
|
||||
loadErrorHandlingPolicy);
|
||||
// Acquire the session once on behalf of the caller to DrmSessionManager - this is the
|
||||
// reference 'assigned' to the caller which they're responsible for releasing. Do this first,
|
||||
|
@ -782,7 +862,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||
// The session has been acquired elsewhere so we want to cancel our timeout.
|
||||
keepaliveSessions.remove(session);
|
||||
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
|
||||
checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -791,7 +871,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) {
|
||||
// Only the internal keep-alive reference remains, so we can start the timeout.
|
||||
keepaliveSessions.add(session);
|
||||
Assertions.checkNotNull(sessionReleasingHandler)
|
||||
checkNotNull(playbackHandler)
|
||||
.postAtTime(
|
||||
() -> session.release(/* eventDispatcher= */ null),
|
||||
session,
|
||||
|
@ -812,7 +892,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
}
|
||||
provisioningSessions.remove(session);
|
||||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
|
||||
checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
|
||||
keepaliveSessions.remove(session);
|
||||
}
|
||||
}
|
||||
|
@ -824,7 +904,78 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||
@Override
|
||||
public void onEvent(
|
||||
ExoMediaDrm md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) {
|
||||
Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
|
||||
checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link DrmSessionReference} that lazily acquires the underlying {@link
|
||||
* DrmSession}.
|
||||
*
|
||||
* <p>A new instance is needed for each reference (compared to maintaining exactly one instance
|
||||
* for each {@link DrmSession}) because each associated {@link
|
||||
* DrmSessionEventListener.EventDispatcher} might be different. The {@link
|
||||
* DrmSessionEventListener.EventDispatcher} is required to implement the zero-arg {@link
|
||||
* DrmSessionReference#release()} method.
|
||||
*/
|
||||
private class PreacquiredSessionReference implements DrmSessionReference {
|
||||
|
||||
@Nullable private final DrmSessionEventListener.EventDispatcher eventDispatcher;
|
||||
|
||||
@Nullable private DrmSession session;
|
||||
private boolean isReleased;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
*
|
||||
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link
|
||||
* #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
|
||||
*/
|
||||
public PreacquiredSessionReference(
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires the underlying session.
|
||||
*
|
||||
* <p>Must be called at most once. Can be called from any thread.
|
||||
*/
|
||||
@RequiresNonNull("playbackHandler")
|
||||
public void acquire(Format format) {
|
||||
playbackHandler.post(
|
||||
() -> {
|
||||
if (prepareCallsCount == 0 || isReleased) {
|
||||
// The manager has been fully released or this reference has already been released.
|
||||
// Abort the acquisition attempt.
|
||||
return;
|
||||
}
|
||||
this.session =
|
||||
acquireSession(
|
||||
checkNotNull(playbackLooper),
|
||||
eventDispatcher,
|
||||
format,
|
||||
/* shouldReleasePreacquiredSessionsBeforeRetrying= */ false);
|
||||
preacquiredSessionReferences.add(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Ensure the underlying session is released immediately if we're already on the playback
|
||||
// thread, to allow a failed session opening to be immediately retried.
|
||||
Util.postOrRun(
|
||||
checkNotNull(playbackHandler),
|
||||
() -> {
|
||||
if (isReleased) {
|
||||
return;
|
||||
}
|
||||
if (session != null) {
|
||||
session.release(eventDispatcher);
|
||||
}
|
||||
preacquiredSessionReferences.remove(this);
|
||||
isReleased = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,19 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||
/** Listener of {@link DrmSessionManager} events. */
|
||||
public interface DrmSessionEventListener {
|
||||
|
||||
/** @deprecated Implement {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} instead. */
|
||||
@Deprecated
|
||||
default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}
|
||||
|
||||
/**
|
||||
* Called each time a drm session is acquired.
|
||||
*
|
||||
* @param windowIndex The window index in the timeline this media period belongs to.
|
||||
* @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
|
||||
* @param state The {@link DrmSession.State} of the session when the acquisition completed.
|
||||
*/
|
||||
default void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}
|
||||
default void onDrmSessionAcquired(
|
||||
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {}
|
||||
|
||||
/**
|
||||
* Called each time keys are loaded.
|
||||
|
@ -149,13 +155,20 @@ public interface DrmSessionEventListener {
|
|||
}
|
||||
}
|
||||
|
||||
/** Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId)}. */
|
||||
public void drmSessionAcquired() {
|
||||
/**
|
||||
* Dispatches {@link #onDrmSessionAcquired(int, MediaPeriodId, int)} and {@link
|
||||
* #onDrmSessionAcquired(int, MediaPeriodId)}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Calls deprecated listener method.
|
||||
public void drmSessionAcquired(@DrmSession.State int state) {
|
||||
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
|
||||
DrmSessionEventListener listener = listenerAndHandler.listener;
|
||||
postOrRun(
|
||||
listenerAndHandler.handler,
|
||||
() -> listener.onDrmSessionAcquired(windowIndex, mediaPeriodId));
|
||||
() -> {
|
||||
listener.onDrmSessionAcquired(windowIndex, mediaPeriodId);
|
||||
listener.onDrmSessionAcquired(windowIndex, mediaPeriodId, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format;
|
|||
/** Manages a DRM session. */
|
||||
public interface DrmSessionManager {
|
||||
|
||||
/**
|
||||
* Represents a single reference count of a {@link DrmSession}, while deliberately not giving
|
||||
* access to the underlying session.
|
||||
*/
|
||||
interface DrmSessionReference {
|
||||
/** A reference that is never populated with an underlying {@link DrmSession}. */
|
||||
DrmSessionReference EMPTY = () -> {};
|
||||
|
||||
/**
|
||||
* Releases the underlying session at most once.
|
||||
*
|
||||
* <p>Can be called from any thread. Calling this method more than once will only release the
|
||||
* underlying session once.
|
||||
*/
|
||||
void release();
|
||||
}
|
||||
|
||||
/** An instance that supports no DRM schemes. */
|
||||
DrmSessionManager DRM_UNSUPPORTED =
|
||||
new DrmSessionManager() {
|
||||
|
@ -81,6 +98,51 @@ public interface DrmSessionManager {
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-acquires a DRM session for the specified {@link Format}.
|
||||
*
|
||||
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(Looper,
|
||||
* DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely,
|
||||
* allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready
|
||||
* in the background.
|
||||
*
|
||||
* <p>The caller must call {@link DrmSessionReference#release()} on the returned instance when
|
||||
* they no longer require the pre-acquisition (i.e. they know they won't be making a matching call
|
||||
* to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near
|
||||
* future).
|
||||
*
|
||||
* <p>This manager may silently release the underlying session in order to allow another operation
|
||||
* to complete. This will result in a subsequent call to {@link #acquireSession(Looper,
|
||||
* DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including
|
||||
* repeating key loads and other async initialization steps.
|
||||
*
|
||||
* <p>The caller must separately call {@link #acquireSession(Looper,
|
||||
* DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for
|
||||
* playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances
|
||||
* are distinct. The caller must release both, and can release the {@link DrmSessionReference}
|
||||
* before the {@link DrmSession} without affecting playback.
|
||||
*
|
||||
* <p>This can be called from any thread.
|
||||
*
|
||||
* <p>Implementations that do not support pre-acquisition always return an empty {@link
|
||||
* DrmSessionReference} instance.
|
||||
*
|
||||
* @param playbackLooper The looper associated with the media playback thread.
|
||||
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
|
||||
* events, and passed on to {@link
|
||||
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
|
||||
* @param format The {@link Format} for which to pre-acquire a {@link DrmSession}.
|
||||
* @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching
|
||||
* {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would
|
||||
* return null.
|
||||
*/
|
||||
default DrmSessionReference preacquireSession(
|
||||
Looper playbackLooper,
|
||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||
Format format) {
|
||||
return DrmSessionReference.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference
|
||||
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is
|
||||
|
|
|
@ -733,11 +733,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
throws ExoPlaybackException {
|
||||
this.currentPlaybackSpeed = currentPlaybackSpeed;
|
||||
this.targetPlaybackSpeed = targetPlaybackSpeed;
|
||||
if (codec != null
|
||||
&& codecDrainAction != DRAIN_ACTION_REINITIALIZE
|
||||
&& getState() != STATE_DISABLED) {
|
||||
updateCodecOperatingRate(codecInputFormat);
|
||||
}
|
||||
updateCodecOperatingRate(codecInputFormat);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1689,6 +1685,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
return CODEC_OPERATING_RATE_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the codec operating rate, or triggers codec release and re-initialization if a
|
||||
* previously set operating rate needs to be cleared.
|
||||
*
|
||||
* @throws ExoPlaybackException If an error occurs releasing or initializing a codec.
|
||||
* @return False if codec release and re-initialization was triggered. True in all other cases.
|
||||
*/
|
||||
protected final boolean updateCodecOperatingRate() throws ExoPlaybackException {
|
||||
return updateCodecOperatingRate(codecInputFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the codec operating rate, or triggers codec release and re-initialization if a
|
||||
* previously set operating rate needs to be cleared.
|
||||
|
@ -1702,6 +1709,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (codec == null
|
||||
|| codecDrainAction == DRAIN_ACTION_REINITIALIZE
|
||||
|| getState() == STATE_DISABLED) {
|
||||
// No need to update the operating rate.
|
||||
return true;
|
||||
}
|
||||
|
||||
float newCodecOperatingRate =
|
||||
getCodecOperatingRateV23(targetPlaybackSpeed, format, getStreamFormats());
|
||||
if (codecOperatingRate == newCodecOperatingRate) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.os.Handler;
|
|||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
@ -290,9 +291,10 @@ public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
|||
// DrmSessionEventListener implementation
|
||||
|
||||
@Override
|
||||
public void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
|
||||
public void onDrmSessionAcquired(
|
||||
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmSessionAcquired();
|
||||
drmEventDispatcher.drmSessionAcquired(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -336,7 +336,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource<Void> {
|
|||
.setTag(tag)
|
||||
.build(),
|
||||
dataSourceFactory,
|
||||
extractorsFactory,
|
||||
() -> new BundledExtractorsAdapter(extractorsFactory),
|
||||
DrmSessionManager.DRM_UNSUPPORTED,
|
||||
loadableLoadErrorHandlingPolicy,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
|
|
|
@ -26,7 +26,14 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
/** Extracts the contents of a container file from a progressive media stream. */
|
||||
/* package */ interface ProgressiveMediaExtractor {
|
||||
public interface ProgressiveMediaExtractor {
|
||||
|
||||
/** Creates {@link ProgressiveMediaExtractor} instances. */
|
||||
interface Factory {
|
||||
|
||||
/** Returns a new {@link ProgressiveMediaExtractor} instance. */
|
||||
ProgressiveMediaExtractor createProgressiveMediaExtractor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the underlying infrastructure for reading from the input.
|
||||
|
|
|
@ -31,7 +31,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
|||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
|
||||
|
@ -147,7 +146,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
/**
|
||||
* @param uri The {@link Uri} of the media stream.
|
||||
* @param dataSource The data source to read the media.
|
||||
* @param extractorsFactory The {@link ExtractorsFactory} to use to read the data source.
|
||||
* @param progressiveMediaExtractor The {@link ProgressiveMediaExtractor} to use to read the data
|
||||
* source.
|
||||
* @param drmSessionManager A {@link DrmSessionManager} to allow DRM interactions.
|
||||
* @param drmEventDispatcher A dispatcher to notify of {@link DrmSessionEventListener} events.
|
||||
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
|
||||
|
@ -168,7 +168,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
public ProgressiveMediaPeriod(
|
||||
Uri uri,
|
||||
DataSource dataSource,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
ProgressiveMediaExtractor progressiveMediaExtractor,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||
|
@ -188,7 +188,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
this.customCacheKey = customCacheKey;
|
||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||
loader = new Loader("ProgressiveMediaPeriod");
|
||||
this.progressiveMediaExtractor = new BundledExtractorsAdapter(extractorsFactory);
|
||||
this.progressiveMediaExtractor = progressiveMediaExtractor;
|
||||
loadCondition = new ConditionVariable();
|
||||
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
|
||||
onContinueLoadingRequestedRunnable =
|
||||
|
|
|
@ -54,7 +54,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
private ExtractorsFactory extractorsFactory;
|
||||
private ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
|
||||
private boolean usingCustomDrmSessionManagerProvider;
|
||||
private DrmSessionManagerProvider drmSessionManagerProvider;
|
||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
|
@ -72,15 +72,26 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
this(dataSourceFactory, new DefaultExtractorsFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to {@link #Factory(DataSource.Factory, ProgressiveMediaExtractor.Factory) new
|
||||
* Factory(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory)}.
|
||||
*/
|
||||
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
|
||||
this(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new factory for {@link ProgressiveMediaSource}s.
|
||||
*
|
||||
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
|
||||
* @param extractorsFactory A factory for extractors used to extract media from its container.
|
||||
* @param progressiveMediaExtractorFactory A factory for the {@link ProgressiveMediaExtractor}
|
||||
* to extract media from its container.
|
||||
*/
|
||||
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
|
||||
public Factory(
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.extractorsFactory = extractorsFactory;
|
||||
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
|
||||
drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
|
||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||
|
@ -93,8 +104,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
*/
|
||||
@Deprecated
|
||||
public Factory setExtractorsFactory(@Nullable ExtractorsFactory extractorsFactory) {
|
||||
this.extractorsFactory =
|
||||
extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory();
|
||||
this.progressiveMediaExtractorFactory =
|
||||
() ->
|
||||
new BundledExtractorsAdapter(
|
||||
extractorsFactory != null ? extractorsFactory : new DefaultExtractorsFactory());
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -220,7 +233,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
return new ProgressiveMediaSource(
|
||||
mediaItem,
|
||||
dataSourceFactory,
|
||||
extractorsFactory,
|
||||
progressiveMediaExtractorFactory,
|
||||
drmSessionManagerProvider.get(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
|
@ -241,7 +254,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
private final MediaItem mediaItem;
|
||||
private final MediaItem.PlaybackProperties playbackProperties;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final ExtractorsFactory extractorsFactory;
|
||||
private final ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory;
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
|
||||
private final int continueLoadingCheckIntervalBytes;
|
||||
|
@ -256,14 +269,14 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
/* package */ ProgressiveMediaSource(
|
||||
MediaItem mediaItem,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
ProgressiveMediaExtractor.Factory progressiveMediaExtractorFactory,
|
||||
DrmSessionManager drmSessionManager,
|
||||
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
|
||||
int continueLoadingCheckIntervalBytes) {
|
||||
this.playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||
this.mediaItem = mediaItem;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.extractorsFactory = extractorsFactory;
|
||||
this.progressiveMediaExtractorFactory = progressiveMediaExtractorFactory;
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
|
||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||
|
@ -308,7 +321,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||
return new ProgressiveMediaPeriod(
|
||||
playbackProperties.uri,
|
||||
dataSource,
|
||||
extractorsFactory,
|
||||
progressiveMediaExtractorFactory.createProgressiveMediaExtractor(),
|
||||
drmSessionManager,
|
||||
createDrmEventDispatcher(id),
|
||||
loadableLoadErrorHandlingPolicy,
|
||||
|
|
|
@ -29,8 +29,12 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
|
||||
import com.google.android.exoplayer2.upstream.DataReader;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
@ -41,6 +45,41 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
*/
|
||||
public final class BundledChunkExtractor implements ExtractorOutput, ChunkExtractor {
|
||||
|
||||
/** {@link ChunkExtractor.Factory} for instances of this class. */
|
||||
public static final ChunkExtractor.Factory FACTORY =
|
||||
(primaryTrackType,
|
||||
format,
|
||||
enableEventMessageTrack,
|
||||
closedCaptionFormats,
|
||||
playerEmsgTrackOutput) -> {
|
||||
@Nullable String containerMimeType = format.containerMimeType;
|
||||
Extractor extractor;
|
||||
if (MimeTypes.isText(containerMimeType)) {
|
||||
if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
|
||||
// RawCC is special because it's a text specific container format.
|
||||
extractor = new RawCcExtractor(format);
|
||||
} else {
|
||||
// All other text types are raw formats that do not need an extractor.
|
||||
return null;
|
||||
}
|
||||
} else if (MimeTypes.isMatroska(containerMimeType)) {
|
||||
extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);
|
||||
} else {
|
||||
int flags = 0;
|
||||
if (enableEventMessageTrack) {
|
||||
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
|
||||
}
|
||||
extractor =
|
||||
new FragmentedMp4Extractor(
|
||||
flags,
|
||||
/* timestampAdjuster= */ null,
|
||||
/* sideloadedTrack= */ null,
|
||||
closedCaptionFormats,
|
||||
playerEmsgTrackOutput);
|
||||
}
|
||||
return new BundledChunkExtractor(extractor, primaryTrackType, format);
|
||||
};
|
||||
|
||||
private static final PositionHolder POSITION_HOLDER = new PositionHolder();
|
||||
|
||||
private final Extractor extractor;
|
||||
|
|
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ChunkIndex;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Extracts samples and track {@link Format Formats} from chunks.
|
||||
|
@ -31,6 +32,27 @@ import java.io.IOException;
|
|||
*/
|
||||
public interface ChunkExtractor {
|
||||
|
||||
/** Creates {@link ChunkExtractor} instances. */
|
||||
interface Factory {
|
||||
|
||||
/**
|
||||
* Returns a new {@link ChunkExtractor} instance.
|
||||
*
|
||||
* @param primaryTrackType The type of the primary track. One of {@link C C.TRACK_TYPE_*}.
|
||||
* @param representationFormat The format of the representation to extract from.
|
||||
* @param enableEventMessageTrack Whether to enable the event message track.
|
||||
* @param closedCaptionFormats The {@link Format Formats} of the Closed-Caption tracks.
|
||||
* @return A new {@link ChunkExtractor} instance, or null if not applicable.
|
||||
*/
|
||||
@Nullable
|
||||
ChunkExtractor createProgressiveMediaExtractor(
|
||||
int primaryTrackType,
|
||||
Format representationFormat,
|
||||
boolean enableEventMessageTrack,
|
||||
List<Format> closedCaptionFormats,
|
||||
@Nullable TrackOutput playerEmsgTrackOutput);
|
||||
}
|
||||
|
||||
/** Provides {@link TrackOutput} instances to be written to during extraction. */
|
||||
interface TrackOutputProvider {
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import static com.google.android.exoplayer2.source.mediaparser.MediaParserUtil.P
|
|||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaParser;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
@ -49,6 +50,25 @@ import java.util.List;
|
|||
@RequiresApi(30)
|
||||
public final class MediaParserChunkExtractor implements ChunkExtractor {
|
||||
|
||||
// Maximum TAG length is 23 characters.
|
||||
private static final String TAG = "MediaPrsrChunkExtractor";
|
||||
|
||||
public static final ChunkExtractor.Factory FACTORY =
|
||||
(primaryTrackType,
|
||||
format,
|
||||
enableEventMessageTrack,
|
||||
closedCaptionFormats,
|
||||
playerEmsgTrackOutput) -> {
|
||||
if (!MimeTypes.isText(format.containerMimeType)) {
|
||||
// Container is either Matroska or Fragmented MP4.
|
||||
return new MediaParserChunkExtractor(primaryTrackType, format, closedCaptionFormats);
|
||||
} else {
|
||||
// This is either RAWCC (unsupported) or a text track that does not require an extractor.
|
||||
Log.w(TAG, "Ignoring an unsupported text track.");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private final OutputConsumerAdapterV30 outputConsumerAdapter;
|
||||
private final InputReaderAdapterV30 inputReaderAdapter;
|
||||
private final MediaParser mediaParser;
|
||||
|
|
|
@ -318,8 +318,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||
}
|
||||
if (style.fontSize != Cue.DIMEN_UNSET && screenHeight != Cue.DIMEN_UNSET) {
|
||||
cue.setTextSize(
|
||||
style.fontSize / screenHeight,
|
||||
Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
|
||||
style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
|
||||
}
|
||||
if (style.bold && style.italic) {
|
||||
spannableText.setSpan(
|
||||
|
|
|
@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||
break;
|
||||
}
|
||||
}
|
||||
return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET)
|
||||
return (startTimeIndex != C.INDEX_UNSET
|
||||
&& endTimeIndex != C.INDEX_UNSET
|
||||
&& textIndex != C.INDEX_UNSET)
|
||||
? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length)
|
||||
: null;
|
||||
}
|
||||
|
|
|
@ -125,11 +125,21 @@ import java.util.regex.Pattern;
|
|||
try {
|
||||
return new SsaStyle(
|
||||
styleValues[format.nameIndex].trim(),
|
||||
parseAlignment(styleValues[format.alignmentIndex].trim()),
|
||||
parseColor(styleValues[format.primaryColorIndex].trim()),
|
||||
parseFontSize(styleValues[format.fontSizeIndex].trim()),
|
||||
parseBold(styleValues[format.boldIndex].trim()),
|
||||
parseItalic(styleValues[format.italicIndex].trim()));
|
||||
format.alignmentIndex != C.INDEX_UNSET
|
||||
? parseAlignment(styleValues[format.alignmentIndex].trim())
|
||||
: SSA_ALIGNMENT_UNKNOWN,
|
||||
format.primaryColorIndex != C.INDEX_UNSET
|
||||
? parseColor(styleValues[format.primaryColorIndex].trim())
|
||||
: null,
|
||||
format.fontSizeIndex != C.INDEX_UNSET
|
||||
? parseFontSize(styleValues[format.fontSizeIndex].trim())
|
||||
: Cue.DIMEN_UNSET,
|
||||
format.boldIndex != C.INDEX_UNSET)
|
||||
? parseBold(styleValues[format.boldIndex].trim())
|
||||
: false,
|
||||
format.italicIndex != C.INDEX_UNSET)
|
||||
? parseItalic(styleValues[format.italicIndex].trim())
|
||||
: false);
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
|
||||
return null;
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
|
|||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/** Track selection related utility methods. */
|
||||
|
@ -97,4 +98,20 @@ public final class TrackSelectionUtil {
|
|||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Returns if a {@link TrackSelectionArray} has at least one track of the given type. */
|
||||
public static boolean hasTrackOfType(TrackSelectionArray trackSelections, int trackType) {
|
||||
for (int i = 0; i < trackSelections.length; i++) {
|
||||
@Nullable TrackSelection trackSelection = trackSelections.get(i);
|
||||
if (trackSelection == null) {
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < trackSelection.length(); j++) {
|
||||
if (MimeTypes.getTrackType(trackSelection.getFormat(j).sampleMimeType) == trackType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ public final class AssetDataSource extends BaseDataSource {
|
|||
if (skipped < dataSpec.position) {
|
||||
// assetManager.open() returns an AssetInputStream, whose skip() implementation only skips
|
||||
// fewer bytes than requested if the skip is beyond the end of the asset's data.
|
||||
throw new EOFException();
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesRemaining = dataSpec.length;
|
||||
|
|
|
@ -47,13 +47,13 @@ public final class ByteArrayDataSource extends BaseDataSource {
|
|||
public long open(DataSpec dataSpec) throws IOException {
|
||||
uri = dataSpec.uri;
|
||||
transferInitializing(dataSpec);
|
||||
readPosition = (int) dataSpec.position;
|
||||
bytesRemaining = (int) ((dataSpec.length == C.LENGTH_UNSET)
|
||||
? (data.length - dataSpec.position) : dataSpec.length);
|
||||
if (bytesRemaining <= 0 || readPosition + bytesRemaining > data.length) {
|
||||
throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length
|
||||
+ "], length: " + data.length);
|
||||
if (dataSpec.position >= data.length) {
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
readPosition = (int) dataSpec.position;
|
||||
bytesRemaining =
|
||||
(int)
|
||||
(dataSpec.length == C.LENGTH_UNSET ? data.length - dataSpec.position : dataSpec.length);
|
||||
opened = true;
|
||||
transferStarted(dataSpec);
|
||||
return bytesRemaining;
|
||||
|
|
|
@ -80,7 +80,7 @@ public final class ContentDataSource extends BaseDataSource {
|
|||
if (skipped != dataSpec.position) {
|
||||
// We expect the skip to be satisfied in full. If it isn't then we're probably trying to
|
||||
// skip beyond the end of the data.
|
||||
throw new EOFException();
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesRemaining = dataSpec.length;
|
||||
|
@ -96,13 +96,13 @@ public final class ContentDataSource extends BaseDataSource {
|
|||
} else {
|
||||
bytesRemaining = channelSize - channel.position();
|
||||
if (bytesRemaining < 0) {
|
||||
throw new EOFException();
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bytesRemaining = assetFileDescriptorLength - skipped;
|
||||
if (bytesRemaining < 0) {
|
||||
throw new EOFException();
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ public final class DataSchemeDataSource extends BaseDataSource {
|
|||
}
|
||||
endPosition =
|
||||
dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;
|
||||
if (endPosition > data.length || readPosition > endPosition) {
|
||||
if (readPosition >= endPosition) {
|
||||
data = null;
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.text.TextUtils;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.EOFException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
@ -91,7 +90,7 @@ public final class FileDataSource extends BaseDataSource {
|
|||
bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position
|
||||
: dataSpec.length;
|
||||
if (bytesRemaining < 0) {
|
||||
throw new EOFException();
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new FileDataSourceException(e);
|
||||
|
|
|
@ -60,7 +60,7 @@ public final class RawResourceDataSource extends BaseDataSource {
|
|||
super(message);
|
||||
}
|
||||
|
||||
public RawResourceDataSourceException(IOException e) {
|
||||
public RawResourceDataSourceException(Throwable e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
@ -133,21 +133,39 @@ public final class RawResourceDataSource extends BaseDataSource {
|
|||
}
|
||||
|
||||
transferInitializing(dataSpec);
|
||||
AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId);
|
||||
|
||||
AssetFileDescriptor assetFileDescriptor;
|
||||
try {
|
||||
assetFileDescriptor = resources.openRawResourceFd(resourceId);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
throw new RawResourceDataSourceException(e);
|
||||
}
|
||||
|
||||
this.assetFileDescriptor = assetFileDescriptor;
|
||||
if (assetFileDescriptor == null) {
|
||||
throw new RawResourceDataSourceException("Resource is compressed: " + uri);
|
||||
}
|
||||
|
||||
long assetFileDescriptorLength = assetFileDescriptor.getLength();
|
||||
FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
|
||||
this.inputStream = inputStream;
|
||||
try {
|
||||
// We can't rely only on the "skipped < dataSpec.position" check below to detect whether the
|
||||
// position is beyond the end of the resource being read. This is because the file will
|
||||
// typically contain multiple resources, and there's nothing to prevent InputStream.skip()
|
||||
// from succeeding by skipping into the data of the next resource. Hence we also need to check
|
||||
// against the resource length explicitly, which is guaranteed to be set unless the resource
|
||||
// extends to the end of the file.
|
||||
if (assetFileDescriptorLength != AssetFileDescriptor.UNKNOWN_LENGTH
|
||||
&& dataSpec.position > assetFileDescriptorLength) {
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
inputStream.skip(assetFileDescriptor.getStartOffset());
|
||||
long skipped = inputStream.skip(dataSpec.position);
|
||||
if (skipped < dataSpec.position) {
|
||||
// We expect the skip to be satisfied in full. If it isn't then we're probably trying to
|
||||
// skip beyond the end of the data.
|
||||
throw new EOFException();
|
||||
// read beyond the end of the last resource in the file.
|
||||
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RawResourceDataSourceException(e);
|
||||
|
@ -156,7 +174,6 @@ public final class RawResourceDataSource extends BaseDataSource {
|
|||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesRemaining = dataSpec.length;
|
||||
} else {
|
||||
long assetFileDescriptorLength = assetFileDescriptor.getLength();
|
||||
// If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
|
||||
bytesRemaining =
|
||||
assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH
|
||||
|
|
|
@ -55,7 +55,6 @@ public final class CacheWriter {
|
|||
private final byte[] temporaryBuffer;
|
||||
@Nullable private final ProgressListener progressListener;
|
||||
|
||||
private boolean initialized;
|
||||
private long nextPosition;
|
||||
private long endPosition;
|
||||
private long bytesCached;
|
||||
|
@ -118,18 +117,15 @@ public final class CacheWriter {
|
|||
public void cache() throws IOException {
|
||||
throwIfCanceled();
|
||||
|
||||
if (!initialized) {
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
endPosition = dataSpec.position + dataSpec.length;
|
||||
} else {
|
||||
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey));
|
||||
endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength;
|
||||
}
|
||||
bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length);
|
||||
if (progressListener != null) {
|
||||
progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0);
|
||||
}
|
||||
initialized = true;
|
||||
bytesCached = cache.getCachedBytes(cacheKey, dataSpec.position, dataSpec.length);
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
endPosition = dataSpec.position + dataSpec.length;
|
||||
} else {
|
||||
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey));
|
||||
endPosition = contentLength == C.LENGTH_UNSET ? C.POSITION_UNSET : contentLength;
|
||||
}
|
||||
if (progressListener != null) {
|
||||
progressListener.onProgress(getLength(), bytesCached, /* newBytesCached= */ 0);
|
||||
}
|
||||
|
||||
while (endPosition == C.POSITION_UNSET || nextPosition < endPosition) {
|
||||
|
@ -158,42 +154,50 @@ public final class CacheWriter {
|
|||
*/
|
||||
private long readBlockToCache(long position, long length) throws IOException {
|
||||
boolean isLastBlock = position + length == endPosition || length == C.LENGTH_UNSET;
|
||||
try {
|
||||
long resolvedLength = C.LENGTH_UNSET;
|
||||
boolean isDataSourceOpen = false;
|
||||
if (length != C.LENGTH_UNSET) {
|
||||
// If the length is specified, try to open the data source with a bounded request to avoid
|
||||
// the underlying network stack requesting more data than required.
|
||||
try {
|
||||
DataSpec boundedDataSpec =
|
||||
dataSpec.buildUpon().setPosition(position).setLength(length).build();
|
||||
resolvedLength = dataSource.open(boundedDataSpec);
|
||||
isDataSourceOpen = true;
|
||||
} catch (IOException exception) {
|
||||
if (allowShortContent
|
||||
&& isLastBlock
|
||||
&& DataSourceException.isCausedByPositionOutOfRange(exception)) {
|
||||
// The length of the request exceeds the length of the content. If we allow shorter
|
||||
// content and are reading the last block, fall through and try again with an unbounded
|
||||
// request to read up to the end of the content.
|
||||
Util.closeQuietly(dataSource);
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
long resolvedLength = C.LENGTH_UNSET;
|
||||
boolean isDataSourceOpen = false;
|
||||
if (length != C.LENGTH_UNSET) {
|
||||
// If the length is specified, try to open the data source with a bounded request to avoid
|
||||
// the underlying network stack requesting more data than required.
|
||||
DataSpec boundedDataSpec =
|
||||
dataSpec.buildUpon().setPosition(position).setLength(length).build();
|
||||
try {
|
||||
resolvedLength = dataSource.open(boundedDataSpec);
|
||||
isDataSourceOpen = true;
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(dataSource);
|
||||
if (allowShortContent
|
||||
&& isLastBlock
|
||||
&& DataSourceException.isCausedByPositionOutOfRange(e)) {
|
||||
// The length of the request exceeds the length of the content. If we allow shorter
|
||||
// content and are reading the last block, fall through and try again with an unbounded
|
||||
// request to read up to the end of the content.
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!isDataSourceOpen) {
|
||||
// Either the length was unspecified, or we allow short content and our attempt to open the
|
||||
// DataSource with the specified length failed.
|
||||
throwIfCanceled();
|
||||
DataSpec unboundedDataSpec =
|
||||
dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build();
|
||||
}
|
||||
|
||||
if (!isDataSourceOpen) {
|
||||
// Either the length was unspecified, or we allow short content and our attempt to open the
|
||||
// DataSource with the specified length failed.
|
||||
throwIfCanceled();
|
||||
DataSpec unboundedDataSpec =
|
||||
dataSpec.buildUpon().setPosition(position).setLength(C.LENGTH_UNSET).build();
|
||||
try {
|
||||
resolvedLength = dataSource.open(unboundedDataSpec);
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(dataSource);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
int totalBytesRead = 0;
|
||||
try {
|
||||
if (isLastBlock && resolvedLength != C.LENGTH_UNSET) {
|
||||
onRequestEndPosition(position + resolvedLength);
|
||||
}
|
||||
int totalBytesRead = 0;
|
||||
int bytesRead = 0;
|
||||
while (bytesRead != C.RESULT_END_OF_INPUT) {
|
||||
throwIfCanceled();
|
||||
|
@ -206,10 +210,16 @@ public final class CacheWriter {
|
|||
if (isLastBlock) {
|
||||
onRequestEndPosition(position + totalBytesRead);
|
||||
}
|
||||
return totalBytesRead;
|
||||
} finally {
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(dataSource);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Util.closeQuietly(dataSource) is not used here because it's important that an exception is
|
||||
// thrown if DataSource.close fails. This is because there's no way of knowing whether the block
|
||||
// was successfully cached in this case.
|
||||
dataSource.close();
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
private void onRequestEndPosition(long endPosition) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
|||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.source.LoadEventInfo;
|
||||
import com.google.android.exoplayer2.source.MediaLoadData;
|
||||
|
@ -479,8 +480,8 @@ public class EventLogger implements AnalyticsListener {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionAcquired(EventTime eventTime) {
|
||||
logd(eventTime, "drmSessionAcquired");
|
||||
public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {
|
||||
logd(eventTime, "drmSessionAcquired", "state=" + state);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -69,11 +69,8 @@ public interface VideoRendererEventListener {
|
|||
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
|
||||
* have a decoder.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
default void onVideoInputFormatChanged(
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
onVideoInputFormatChanged(format);
|
||||
}
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
|
||||
|
||||
/**
|
||||
* Called to report the number of frames dropped by the renderer. Dropped frames are reported
|
||||
|
@ -133,7 +130,12 @@ public interface VideoRendererEventListener {
|
|||
*
|
||||
* @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
|
||||
* the renderer renders to something that isn't a {@link Surface}.
|
||||
* @param renderTimeMs The {@link SystemClock#elapsedRealtime()} when the frame was rendered.
|
||||
*/
|
||||
default void onRenderedFirstFrame(@Nullable Surface surface, long renderTimeMs) {}
|
||||
|
||||
/** @deprecated Use {@link #onRenderedFirstFrame(Surface, long)}. */
|
||||
@Deprecated
|
||||
default void onRenderedFirstFrame(@Nullable Surface surface) {}
|
||||
|
||||
/**
|
||||
|
@ -205,11 +207,15 @@ public interface VideoRendererEventListener {
|
|||
* Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format,
|
||||
* DecoderReuseEvaluation)}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Calling deprecated listener method.
|
||||
public void inputFormatChanged(
|
||||
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
|
||||
if (handler != null) {
|
||||
handler.post(
|
||||
() -> castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation));
|
||||
() -> {
|
||||
castNonNull(listener).onVideoInputFormatChanged(format);
|
||||
castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,10 +251,16 @@ public interface VideoRendererEventListener {
|
|||
}
|
||||
}
|
||||
|
||||
/** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */
|
||||
/** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface, long)}. */
|
||||
public void renderedFirstFrame(@Nullable Surface surface) {
|
||||
if (handler != null) {
|
||||
handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface));
|
||||
// TODO: Replace this timestamp with the actual frame release time.
|
||||
long renderTimeMs = SystemClock.elapsedRealtime();
|
||||
handler.post(
|
||||
() -> {
|
||||
castNonNull(listener).onRenderedFirstFrame(surface);
|
||||
castNonNull(listener).onRenderedFirstFrame(surface, renderTimeMs);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8605,6 +8605,20 @@ public final class ExoPlayerTest {
|
|||
assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void playerIdle_withSetPlaybackSpeed_usesPlaybackParameterSpeedWithPitchUnchanged() {
|
||||
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1, /* pitch= */ 2));
|
||||
Player.EventListener mockListener = mock(Player.EventListener.class);
|
||||
player.addListener(mockListener);
|
||||
player.prepare();
|
||||
|
||||
player.setPlaybackSpeed(2);
|
||||
|
||||
verify(mockListener)
|
||||
.onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2, /* pitch= */ 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
|
||||
throws Exception {
|
||||
|
|
|
@ -80,6 +80,7 @@ import com.google.android.exoplayer2.Timeline.Window;
|
|||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.MediaDrmCallback;
|
||||
|
@ -1700,12 +1701,12 @@ public final class AnalyticsCollectorTest {
|
|||
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
|
||||
verify(listener, atLeastOnce())
|
||||
.onVideoDecoderInitialized(
|
||||
individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong());
|
||||
individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong());
|
||||
ArgumentCaptor<AnalyticsListener.EventTime> individualAudioDecoderInitializedEventTimes =
|
||||
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
|
||||
verify(listener, atLeastOnce())
|
||||
.onAudioDecoderInitialized(
|
||||
individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong());
|
||||
individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong(), anyLong());
|
||||
ArgumentCaptor<AnalyticsListener.EventTime> individualVideoDisabledEventTimes =
|
||||
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
|
||||
verify(listener, atLeastOnce())
|
||||
|
@ -1717,7 +1718,7 @@ public final class AnalyticsCollectorTest {
|
|||
ArgumentCaptor<AnalyticsListener.EventTime> individualRenderedFirstFrameEventTimes =
|
||||
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
|
||||
verify(listener, atLeastOnce())
|
||||
.onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any());
|
||||
.onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any(), anyLong());
|
||||
ArgumentCaptor<AnalyticsListener.EventTime> individualVideoSizeChangedEventTimes =
|
||||
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
|
||||
verify(listener, atLeastOnce())
|
||||
|
@ -2183,7 +2184,10 @@ public final class AnalyticsCollectorTest {
|
|||
|
||||
@Override
|
||||
public void onAudioDecoderInitialized(
|
||||
EventTime eventTime, String decoderName, long initializationDurationMs) {
|
||||
EventTime eventTime,
|
||||
String decoderName,
|
||||
long initializedTimestampMs,
|
||||
long initializationDurationMs) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_AUDIO_DECODER_INITIALIZED, eventTime));
|
||||
}
|
||||
|
||||
|
@ -2220,7 +2224,10 @@ public final class AnalyticsCollectorTest {
|
|||
|
||||
@Override
|
||||
public void onVideoDecoderInitialized(
|
||||
EventTime eventTime, String decoderName, long initializationDurationMs) {
|
||||
EventTime eventTime,
|
||||
String decoderName,
|
||||
long initializedTimestampMs,
|
||||
long initializationDurationMs) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_VIDEO_DECODER_INITIALIZED, eventTime));
|
||||
}
|
||||
|
||||
|
@ -2246,7 +2253,8 @@ public final class AnalyticsCollectorTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {
|
||||
public void onRenderedFirstFrame(
|
||||
EventTime eventTime, @Nullable Surface surface, long renderTimeMs) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime));
|
||||
}
|
||||
|
||||
|
@ -2261,7 +2269,7 @@ public final class AnalyticsCollectorTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionAcquired(EventTime eventTime) {
|
||||
public void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {
|
||||
reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_ACQUIRED, eventTime));
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ public final class DefaultAudioSinkTest {
|
|||
new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor),
|
||||
/* enableFloatOutput= */ false,
|
||||
/* enableAudioTrackPlaybackParams= */ false,
|
||||
/* enableOffload= */ false);
|
||||
DefaultAudioSink.OFFLOAD_MODE_DISABLED);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat;
|
|||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper;
|
|||
// - Multiple acquisitions & releases for same keys -> multiple requests.
|
||||
// - Provisioning.
|
||||
// - Key denial.
|
||||
// - Handling of ResourceBusyException (indicating session scarcity).
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DefaultDrmSessionManagerTest {
|
||||
|
||||
|
@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest {
|
|||
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void preacquireSession_loadsKeysBeforeFullAcquisition() throws Exception {
|
||||
AtomicInteger keyLoadCount = new AtomicInteger(0);
|
||||
DrmSessionEventListener.EventDispatcher eventDispatcher =
|
||||
new DrmSessionEventListener.EventDispatcher();
|
||||
eventDispatcher.addEventListener(
|
||||
Util.createHandlerForCurrentLooper(),
|
||||
new DrmSessionEventListener() {
|
||||
@Override
|
||||
public void onDrmKeysLoaded(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
keyLoadCount.incrementAndGet();
|
||||
}
|
||||
});
|
||||
FakeExoMediaDrm.LicenseServer licenseServer =
|
||||
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
|
||||
DrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||
// Disable keepalive
|
||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||
.build(/* mediaDrmCallback= */ licenseServer);
|
||||
|
||||
drmSessionManager.prepare();
|
||||
|
||||
DrmSessionManager.DrmSessionReference sessionReference =
|
||||
drmSessionManager.preacquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
eventDispatcher,
|
||||
FORMAT_WITH_DRM_INIT_DATA);
|
||||
|
||||
// Wait for the key load event to propagate, indicating the pre-acquired session is in
|
||||
// STATE_OPENED_WITH_KEYS.
|
||||
while (keyLoadCount.get() == 0) {
|
||||
// Allow the key response to be handled.
|
||||
ShadowLooper.idleMainLooper();
|
||||
}
|
||||
|
||||
DrmSession drmSession =
|
||||
checkNotNull(
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
FORMAT_WITH_DRM_INIT_DATA));
|
||||
|
||||
// Without idling the main/playback looper, we assert the session is already in OPENED_WITH_KEYS
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
assertThat(keyLoadCount.get()).isEqualTo(1);
|
||||
|
||||
// After releasing our concrete session reference, the session is held open by the pre-acquired
|
||||
// reference.
|
||||
drmSession.release(/* eventDispatcher= */ null);
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
|
||||
// Releasing the pre-acquired reference allows the session to be fully released.
|
||||
sessionReference.release();
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void
|
||||
preacquireSession_releaseBeforeUnderlyingAcquisitionCompletesReleasesSessionOnceAcquired()
|
||||
throws Exception {
|
||||
FakeExoMediaDrm.LicenseServer licenseServer =
|
||||
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
|
||||
DrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||
// Disable keepalive
|
||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||
.build(/* mediaDrmCallback= */ licenseServer);
|
||||
|
||||
drmSessionManager.prepare();
|
||||
|
||||
DrmSessionManager.DrmSessionReference sessionReference =
|
||||
drmSessionManager.preacquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
FORMAT_WITH_DRM_INIT_DATA);
|
||||
|
||||
// Release the pre-acquired reference before the underlying session has had a chance to be
|
||||
// constructed.
|
||||
sessionReference.release();
|
||||
|
||||
// Acquiring the same session triggers a second key load (because the pre-acquired session was
|
||||
// fully released).
|
||||
DrmSession drmSession =
|
||||
checkNotNull(
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
FORMAT_WITH_DRM_INIT_DATA));
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED);
|
||||
|
||||
waitForOpenedWithKeys(drmSession);
|
||||
|
||||
drmSession.release(/* eventDispatcher= */ null);
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void preacquireSession_releaseManagerBeforeAcquisition_acquisitionDoesntHappen()
|
||||
throws Exception {
|
||||
FakeExoMediaDrm.LicenseServer licenseServer =
|
||||
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
|
||||
DrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||
// Disable keepalive
|
||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||
.build(/* mediaDrmCallback= */ licenseServer);
|
||||
|
||||
drmSessionManager.prepare();
|
||||
|
||||
DrmSessionManager.DrmSessionReference sessionReference =
|
||||
drmSessionManager.preacquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
FORMAT_WITH_DRM_INIT_DATA);
|
||||
|
||||
// Release the manager before the underlying session has had a chance to be constructed. This
|
||||
// will release all pre-acquired sessions.
|
||||
drmSessionManager.release();
|
||||
|
||||
// Allow the acquisition event to be handled on the main/playback thread.
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
// Re-prepare the manager so we can fully acquire the same session, and check the previous
|
||||
// pre-acquisition didn't do anything.
|
||||
drmSessionManager.prepare();
|
||||
DrmSession drmSession =
|
||||
checkNotNull(
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
FORMAT_WITH_DRM_INIT_DATA));
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED);
|
||||
waitForOpenedWithKeys(drmSession);
|
||||
|
||||
drmSession.release(/* eventDispatcher= */ null);
|
||||
// If the (still unreleased) pre-acquired session above was linked to the same underlying
|
||||
// session then the state would still be OPENED_WITH_KEYS.
|
||||
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
|
||||
|
||||
// Release the pre-acquired session from above (this is a no-op, but we do it anyway for
|
||||
// correctness).
|
||||
sessionReference.release();
|
||||
drmSessionManager.release();
|
||||
}
|
||||
|
||||
private static void waitForOpenedWithKeys(DrmSession drmSession) {
|
||||
// Check the error first, so we get a meaningful failure if there's been an error.
|
||||
assertThat(drmSession.getError()).isNull();
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider;
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.testutil.FailOnCloseDataSink;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
|
@ -34,6 +35,7 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
|||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
@ -66,7 +68,7 @@ public class ProgressiveDownloaderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void download_afterSingleFailure_succeeds() throws Exception {
|
||||
public void download_afterReadFailure_succeeds() throws Exception {
|
||||
Uri uri = Uri.parse("test:///test.mp4");
|
||||
|
||||
// Fake data has a built in failure after 10 bytes.
|
||||
|
@ -92,6 +94,39 @@ public class ProgressiveDownloaderTest {
|
|||
assertThat(progressListener.bytesDownloaded).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void download_afterWriteFailureOnClose_succeeds() throws Exception {
|
||||
Uri uri = Uri.parse("test:///test.mp4");
|
||||
|
||||
FakeDataSet data = new FakeDataSet();
|
||||
data.newData(uri).appendReadData(1024);
|
||||
DataSource.Factory upstreamDataSource = new FakeDataSource.Factory().setFakeDataSet(data);
|
||||
|
||||
AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true);
|
||||
FailOnCloseDataSink.Factory dataSinkFactory =
|
||||
new FailOnCloseDataSink.Factory(downloadCache, failOnClose);
|
||||
|
||||
MediaItem mediaItem = MediaItem.fromUri(uri);
|
||||
CacheDataSource.Factory cacheDataSourceFactory =
|
||||
new CacheDataSource.Factory()
|
||||
.setCache(downloadCache)
|
||||
.setCacheWriteDataSinkFactory(dataSinkFactory)
|
||||
.setUpstreamDataSourceFactory(upstreamDataSource);
|
||||
ProgressiveDownloader downloader = new ProgressiveDownloader(mediaItem, cacheDataSourceFactory);
|
||||
|
||||
TestProgressListener progressListener = new TestProgressListener();
|
||||
|
||||
// Failure expected after 1024 bytes.
|
||||
assertThrows(IOException.class, () -> downloader.download(progressListener));
|
||||
assertThat(progressListener.bytesDownloaded).isEqualTo(1024);
|
||||
|
||||
failOnClose.set(false);
|
||||
|
||||
// Retry should succeed.
|
||||
downloader.download(progressListener);
|
||||
assertThat(progressListener.bytesDownloaded).isEqualTo(1024);
|
||||
}
|
||||
|
||||
private static final class TestProgressListener implements Downloader.ProgressListener {
|
||||
|
||||
public long bytesDownloaded;
|
||||
|
|
|
@ -24,22 +24,37 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.upstream.AssetDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** Unit test for {@link ProgressiveMediaPeriod}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class ProgressiveMediaPeriodTest {
|
||||
|
||||
@Test
|
||||
public void prepare_updatesSourceInfoBeforeOnPreparedCallback() throws Exception {
|
||||
public void prepareUsingBundledExtractors_updatesSourceInfoBeforeOnPreparedCallback()
|
||||
throws TimeoutException {
|
||||
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
|
||||
new BundledExtractorsAdapter(Mp4Extractor.FACTORY));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 30)
|
||||
public void prepareUsingMediaParser_updatesSourceInfoBeforeOnPreparedCallback()
|
||||
throws TimeoutException {
|
||||
testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(new MediaParserExtractorAdapter());
|
||||
}
|
||||
|
||||
private static void testExtractorsUpdatesSourceInfoBeforeOnPreparedCallback(
|
||||
ProgressiveMediaExtractor extractor) throws TimeoutException {
|
||||
AtomicBoolean sourceInfoRefreshCalled = new AtomicBoolean(false);
|
||||
ProgressiveMediaPeriod.Listener sourceInfoRefreshListener =
|
||||
(durationUs, isSeekable, isLive) -> sourceInfoRefreshCalled.set(true);
|
||||
|
@ -48,7 +63,7 @@ public final class ProgressiveMediaPeriodTest {
|
|||
new ProgressiveMediaPeriod(
|
||||
Uri.parse("asset://android_asset/media/mp4/sample.mp4"),
|
||||
new AssetDataSource(ApplicationProvider.getApplicationContext()),
|
||||
() -> new Extractor[] {new Mp4Extractor()},
|
||||
extractor,
|
||||
DrmSessionManager.DRM_UNSUPPORTED,
|
||||
new DrmSessionEventListener.EventDispatcher()
|
||||
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
|
||||
|
|
|
@ -323,17 +323,18 @@ public final class SsaDecoderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void decodeFontSize() throws IOException{
|
||||
public void decodeFontSize() throws IOException {
|
||||
SsaDecoder decoder = new SsaDecoder();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.textSize).isEqualTo(30f/720f);
|
||||
assertThat(firstCue.textSize).isWithin(1.0e-8f).of(30f / 720f);
|
||||
assertThat(firstCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
assertThat(secondCue.textSize).isEqualTo(72.2f/720f);
|
||||
assertThat(secondCue.textSize).isWithin(1.0e-8f).of(72.2f / 720f);
|
||||
assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
|
||||
}
|
||||
|
||||
|
|
|
@ -87,14 +87,6 @@ public final class ByteArrayDataSourceTest {
|
|||
readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readWithInvalidLength() {
|
||||
// Read more data than is available.
|
||||
readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true);
|
||||
// And with bound.
|
||||
readTestData(TEST_DATA, 1, TEST_DATA.length, 1, 0, 1, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests reading from a {@link ByteArrayDataSource} with various parameters.
|
||||
*
|
||||
|
|
|
@ -50,7 +50,6 @@ public class CacheDataSourceContractTest extends DataSourceContractTest {
|
|||
File file = tempFolder.newFile();
|
||||
Files.write(Paths.get(file.getAbsolutePath()), DATA);
|
||||
simpleUri = Uri.fromFile(file);
|
||||
fileDataSource = new FileDataSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -74,6 +73,7 @@ public class CacheDataSourceContractTest extends DataSourceContractTest {
|
|||
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
|
||||
SimpleCache cache =
|
||||
new SimpleCache(tempFolder, new NoOpCacheEvictor(), TestUtil.getInMemoryDatabaseProvider());
|
||||
fileDataSource = new FileDataSource();
|
||||
return new CacheDataSource(cache, fileDataSource);
|
||||
}
|
||||
|
||||
|
|
|
@ -39,12 +39,12 @@ public class DataSchemeDataSourceContractTest extends DataSourceContractTest {
|
|||
return ImmutableList.of(
|
||||
new TestResource.Builder()
|
||||
.setName("plain text")
|
||||
.setUri(Uri.parse("data:text/plain," + DATA))
|
||||
.setUri("data:text/plain," + DATA)
|
||||
.setExpectedBytes(DATA.getBytes(UTF_8))
|
||||
.build(),
|
||||
new TestResource.Builder()
|
||||
.setName("base64 encoded text")
|
||||
.setUri(Uri.parse("data:text/plain;base64," + BASE64_ENCODED_DATA))
|
||||
.setUri("data:text/plain;base64," + BASE64_ENCODED_DATA)
|
||||
.setExpectedBytes(Base64.decode(BASE64_ENCODED_DATA, Base64.DEFAULT))
|
||||
.build());
|
||||
}
|
||||
|
|
|
@ -107,18 +107,6 @@ public final class DataSchemeDataSourceTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rangeExceedingResourceLengthRequest() throws IOException {
|
||||
try {
|
||||
// Try to open a range exceeding the resource's length.
|
||||
schemeDataDataSource.open(
|
||||
buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11));
|
||||
fail();
|
||||
} catch (DataSourceException e) {
|
||||
assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incorrectScheme() {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.ResolvingDataSource.Resolver;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import org.junit.Before;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** {@link DataSource} contract tests for {@link ResolvingDataSourceContractTest}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ResolvingDataSourceContractTest extends DataSourceContractTest {
|
||||
|
||||
private static final String URI = "test://simple.test";
|
||||
private static final String RESOLVED_URI = "resolved://simple.resolved";
|
||||
|
||||
private byte[] simpleData;
|
||||
private FakeDataSet fakeDataSet;
|
||||
private FakeDataSource fakeDataSource;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
simpleData = TestUtil.buildTestData(/* length= */ 20);
|
||||
fakeDataSet = new FakeDataSet().newData(RESOLVED_URI).appendReadData(simpleData).endData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImmutableList<TestResource> getTestResources() {
|
||||
return ImmutableList.of(
|
||||
new TestResource.Builder()
|
||||
.setName("simple")
|
||||
.setUri(URI)
|
||||
.setExpectedBytes(simpleData)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri getNotFoundUri() {
|
||||
return Uri.parse("test://not-found.test");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DataSource createDataSource() {
|
||||
fakeDataSource = new FakeDataSource(fakeDataSet);
|
||||
return new ResolvingDataSource(
|
||||
fakeDataSource,
|
||||
new Resolver() {
|
||||
@Override
|
||||
public DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException {
|
||||
return URI.equals(dataSpec.uri.toString())
|
||||
? dataSpec.buildUpon().setUri(RESOLVED_URI).build()
|
||||
: dataSpec;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected DataSource getTransferListenerDataSource() {
|
||||
return fakeDataSource;
|
||||
}
|
||||
}
|
|
@ -53,14 +53,18 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
|
|||
return udpDataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean unboundedReadsAreIndefinite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImmutableList<TestResource> getTestResources() {
|
||||
return ImmutableList.of(
|
||||
new TestResource.Builder()
|
||||
.setName("local-udp-unicast-socket")
|
||||
.setUri(Uri.parse("udp://localhost:" + findFreeUdpPort()))
|
||||
.setUri("udp://localhost:" + findFreeUdpPort())
|
||||
.setExpectedBytes(data)
|
||||
.setEndOfInputExpected(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
|
@ -84,6 +88,26 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
|
|||
@Override
|
||||
public void dataSpecWithPositionAndLength_readExpectedRange() {}
|
||||
|
||||
@Test
|
||||
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
|
||||
@Override
|
||||
public void dataSpecWithPositionAtEnd_throwsPositionOutOfRangeException() {}
|
||||
|
||||
@Test
|
||||
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
|
||||
@Override
|
||||
public void dataSpecWithPositionAtEndAndLength_throwsPositionOutOfRangeException() {}
|
||||
|
||||
@Test
|
||||
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
|
||||
@Override
|
||||
public void dataSpecWithPositionOutOfRange_throwsPositionOutOfRangeException() {}
|
||||
|
||||
@Test
|
||||
@Ignore("UdpDataSource doesn't support DataSpec's position or length [internal: b/175856954]")
|
||||
@Override
|
||||
public void dataSpecWithEndPositionOutOfRange_readsToEnd() {}
|
||||
|
||||
/**
|
||||
* Finds a free UDP port in the range of unreserved ports 50000-60000 that can be used from the
|
||||
* test or throws an {@link IllegalStateException} if no port is available.
|
||||
|
|
|
@ -17,84 +17,40 @@ package com.google.android.exoplayer2.upstream.cache;
|
|||
|
||||
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.lang.Math.min;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.FailOnCloseDataSink;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/** Unit tests for {@link CacheWriter}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CacheWriterTest {
|
||||
|
||||
/**
|
||||
* Abstract fake Cache implementation used by the test. This class must be public so Mockito can
|
||||
* create a proxy for it.
|
||||
*/
|
||||
public abstract static class AbstractFakeCache implements Cache {
|
||||
|
||||
// This array is set to alternating length of cached and not cached regions in tests:
|
||||
// spansAndGaps = {<length of 1st cached region>, <length of 1st not cached region>,
|
||||
// <length of 2nd cached region>, <length of 2nd not cached region>, ... }
|
||||
// Ideally it should end with a cached region but it shouldn't matter for any code.
|
||||
private int[] spansAndGaps;
|
||||
private long contentLength;
|
||||
|
||||
private void init() {
|
||||
spansAndGaps = new int[] {};
|
||||
contentLength = C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCachedLength(String key, long position, long length) {
|
||||
if (length == C.LENGTH_UNSET) {
|
||||
length = Long.MAX_VALUE;
|
||||
}
|
||||
for (int i = 0; i < spansAndGaps.length; i++) {
|
||||
int spanOrGap = spansAndGaps[i];
|
||||
if (position < spanOrGap) {
|
||||
long left = min(spanOrGap - position, length);
|
||||
return (i & 1) == 1 ? -left : left;
|
||||
}
|
||||
position -= spanOrGap;
|
||||
}
|
||||
return -length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentMetadata getContentMetadata(String key) {
|
||||
DefaultContentMetadata metadata = new DefaultContentMetadata();
|
||||
ContentMetadataMutations mutations = new ContentMetadataMutations();
|
||||
ContentMetadataMutations.setContentLength(mutations, contentLength);
|
||||
return metadata.copyWithMutationsApplied(mutations);
|
||||
}
|
||||
}
|
||||
|
||||
@Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache;
|
||||
private File tempFolder;
|
||||
private SimpleCache cache;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mockCache.init();
|
||||
tempFolder =
|
||||
Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
|
||||
cache =
|
||||
|
@ -219,6 +175,7 @@ public final class CacheWriterTest {
|
|||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Ignore("Currently broken. See https://github.com/google/ExoPlayer/issues/7326.")
|
||||
@Test
|
||||
public void cacheLengthExceedsActualDataLength() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
|
||||
|
@ -263,6 +220,50 @@ public final class CacheWriterTest {
|
|||
assertThat(DataSourceException.isCausedByPositionOutOfRange(exception)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cache_afterFailureOnClose_succeeds() throws Exception {
|
||||
FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100);
|
||||
FakeDataSource upstreamDataSource = new FakeDataSource(fakeDataSet);
|
||||
|
||||
AtomicBoolean failOnClose = new AtomicBoolean(/* initialValue= */ true);
|
||||
FailOnCloseDataSink dataSink = new FailOnCloseDataSink(cache, failOnClose);
|
||||
|
||||
CacheDataSource cacheDataSource =
|
||||
new CacheDataSource(
|
||||
cache,
|
||||
upstreamDataSource,
|
||||
new FileDataSource(),
|
||||
dataSink,
|
||||
/* flags= */ 0,
|
||||
/* eventListener= */ null);
|
||||
|
||||
CachingCounters counters = new CachingCounters();
|
||||
|
||||
CacheWriter cacheWriter =
|
||||
new CacheWriter(
|
||||
cacheDataSource,
|
||||
new DataSpec(Uri.parse("test_data")),
|
||||
/* allowShortContent= */ false,
|
||||
/* temporaryBuffer= */ null,
|
||||
counters);
|
||||
|
||||
// DataSink.close failing must cause the operation to fail rather than succeed.
|
||||
assertThrows(IOException.class, cacheWriter::cache);
|
||||
// Since all of the bytes were read through the DataSource chain successfully before the sink
|
||||
// was closed, the progress listener will have seen all of the bytes being cached, even though
|
||||
// this may not really be the case.
|
||||
counters.assertValues(
|
||||
/* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100);
|
||||
|
||||
failOnClose.set(false);
|
||||
|
||||
// The bytes will be downloaded again, but cached successfully this time.
|
||||
cacheWriter.cache();
|
||||
counters.assertValues(
|
||||
/* bytesAlreadyCached= */ 0, /* bytesNewlyCached= */ 100, /* contentLength= */ 100);
|
||||
assertCachedData(cache, fakeDataSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cachePolling() throws Exception {
|
||||
final CachingCounters counters = new CachingCounters();
|
||||
|
|
|
@ -26,11 +26,6 @@ import com.google.android.exoplayer2.C;
|
|||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;
|
||||
import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor;
|
||||
|
@ -53,7 +48,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
|
|||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
|
||||
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -180,11 +174,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||
representationHolders[i] =
|
||||
new RepresentationHolder(
|
||||
periodDurationUs,
|
||||
trackType,
|
||||
representation,
|
||||
enableEventMessageTrack,
|
||||
closedCaptionFormats,
|
||||
playerTrackEmsgHandler);
|
||||
BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor(
|
||||
trackType,
|
||||
representation.format,
|
||||
enableEventMessageTrack,
|
||||
closedCaptionFormats,
|
||||
playerTrackEmsgHandler),
|
||||
/* segmentNumShift= */ 0,
|
||||
representation.getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -665,26 +663,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||
private final long segmentNumShift;
|
||||
|
||||
/* package */ RepresentationHolder(
|
||||
long periodDurationUs,
|
||||
int trackType,
|
||||
Representation representation,
|
||||
boolean enableEventMessageTrack,
|
||||
List<Format> closedCaptionFormats,
|
||||
@Nullable TrackOutput playerEmsgTrackOutput) {
|
||||
this(
|
||||
periodDurationUs,
|
||||
representation,
|
||||
createChunkExtractor(
|
||||
trackType,
|
||||
representation,
|
||||
enableEventMessageTrack,
|
||||
closedCaptionFormats,
|
||||
playerEmsgTrackOutput),
|
||||
/* segmentNumShift= */ 0,
|
||||
representation.getIndex());
|
||||
}
|
||||
|
||||
private RepresentationHolder(
|
||||
long periodDurationUs,
|
||||
Representation representation,
|
||||
@Nullable ChunkExtractor chunkExtractor,
|
||||
|
@ -800,40 +778,5 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||
public boolean isSegmentAvailableAtFullNetworkSpeed(long segmentNum, long nowPeriodTimeUs) {
|
||||
return nowPeriodTimeUs == C.TIME_UNSET || getSegmentEndTimeUs(segmentNum) <= nowPeriodTimeUs;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ChunkExtractor createChunkExtractor(
|
||||
int trackType,
|
||||
Representation representation,
|
||||
boolean enableEventMessageTrack,
|
||||
List<Format> closedCaptionFormats,
|
||||
@Nullable TrackOutput playerEmsgTrackOutput) {
|
||||
String containerMimeType = representation.format.containerMimeType;
|
||||
Extractor extractor;
|
||||
if (MimeTypes.isText(containerMimeType)) {
|
||||
if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
|
||||
// RawCC is special because it's a text specific container format.
|
||||
extractor = new RawCcExtractor(representation.format);
|
||||
} else {
|
||||
// All other text types are raw formats that do not need an extractor.
|
||||
return null;
|
||||
}
|
||||
} else if (MimeTypes.isMatroska(containerMimeType)) {
|
||||
extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);
|
||||
} else {
|
||||
int flags = 0;
|
||||
if (enableEventMessageTrack) {
|
||||
flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
|
||||
}
|
||||
extractor =
|
||||
new FragmentedMp4Extractor(
|
||||
flags,
|
||||
/* timestampAdjuster= */ null,
|
||||
/* sideloadedTrack= */ null,
|
||||
closedCaptionFormats,
|
||||
playerEmsgTrackOutput);
|
||||
}
|
||||
return new BundledChunkExtractor(extractor, trackType, representation.format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -913,11 +913,12 @@ public class PlayerControlView extends FrameLayout {
|
|||
timeline.getWindow(player.getCurrentWindowIndex(), window);
|
||||
boolean isSeekable = window.isSeekable;
|
||||
enableSeeking = isSeekable;
|
||||
enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();
|
||||
enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
|
||||
enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
|
||||
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
|
||||
enableNext =
|
||||
window.isLive() || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
|
||||
(window.isLive() && window.isDynamic)
|
||||
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -320,6 +320,7 @@ public class PlayerNotificationManager {
|
|||
private int fastForwardActionIconResourceId;
|
||||
private int previousActionIconResourceId;
|
||||
private int nextActionIconResourceId;
|
||||
@Nullable private String groupKey;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
|
@ -514,6 +515,18 @@ public class PlayerNotificationManager {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The key of the group the media notification should belong to.
|
||||
*
|
||||
* <p>The default is {@code null}
|
||||
*
|
||||
* @return This builder.
|
||||
*/
|
||||
public Builder setGroup(String groupKey) {
|
||||
this.groupKey = groupKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds the {@link PlayerNotificationManager}. */
|
||||
public PlayerNotificationManager build() {
|
||||
if (channelNameResourceId != 0) {
|
||||
|
@ -538,7 +551,8 @@ public class PlayerNotificationManager {
|
|||
rewindActionIconResourceId,
|
||||
fastForwardActionIconResourceId,
|
||||
previousActionIconResourceId,
|
||||
nextActionIconResourceId);
|
||||
nextActionIconResourceId,
|
||||
groupKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -662,6 +676,7 @@ public class PlayerNotificationManager {
|
|||
private int visibility;
|
||||
@Priority private int priority;
|
||||
private boolean useChronometer;
|
||||
@Nullable private String groupKey;
|
||||
|
||||
/** @deprecated Use the {@link Builder} instead. */
|
||||
@SuppressWarnings("deprecation")
|
||||
|
@ -805,7 +820,8 @@ public class PlayerNotificationManager {
|
|||
R.drawable.exo_notification_rewind,
|
||||
R.drawable.exo_notification_fastforward,
|
||||
R.drawable.exo_notification_previous,
|
||||
R.drawable.exo_notification_next);
|
||||
R.drawable.exo_notification_next,
|
||||
null);
|
||||
}
|
||||
|
||||
private PlayerNotificationManager(
|
||||
|
@ -822,7 +838,8 @@ public class PlayerNotificationManager {
|
|||
int rewindActionIconResourceId,
|
||||
int fastForwardActionIconResourceId,
|
||||
int previousActionIconResourceId,
|
||||
int nextActionIconResourceId) {
|
||||
int nextActionIconResourceId,
|
||||
@Nullable String groupKey) {
|
||||
context = context.getApplicationContext();
|
||||
this.context = context;
|
||||
this.channelId = channelId;
|
||||
|
@ -831,6 +848,7 @@ public class PlayerNotificationManager {
|
|||
this.notificationListener = notificationListener;
|
||||
this.customActionReceiver = customActionReceiver;
|
||||
this.smallIconResourceId = smallIconResourceId;
|
||||
this.groupKey = groupKey;
|
||||
controlDispatcher = new DefaultControlDispatcher();
|
||||
window = new Timeline.Window();
|
||||
instanceId = instanceIdCounter++;
|
||||
|
@ -1407,6 +1425,10 @@ public class PlayerNotificationManager {
|
|||
setLargeIcon(builder, largeIcon);
|
||||
builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player));
|
||||
|
||||
if (groupKey != null) {
|
||||
builder.setGroup(groupKey);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
@ -1437,10 +1459,13 @@ public class PlayerNotificationManager {
|
|||
Timeline timeline = player.getCurrentTimeline();
|
||||
if (!timeline.isEmpty() && !player.isPlayingAd()) {
|
||||
timeline.getWindow(player.getCurrentWindowIndex(), window);
|
||||
enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();
|
||||
enableRewind = controlDispatcher.isRewindEnabled();
|
||||
enableFastForward = controlDispatcher.isFastForwardEnabled();
|
||||
enableNext = window.isDynamic || player.hasNext();
|
||||
boolean isSeekable = window.isSeekable;
|
||||
enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
|
||||
enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
|
||||
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
|
||||
enableNext =
|
||||
(window.isLive() && window.isDynamic)
|
||||
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
|
||||
}
|
||||
|
||||
List<String> stringActions = new ArrayList<>();
|
||||
|
|
|
@ -58,6 +58,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
|
|||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.TextOutput;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionUtil;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
|
||||
import com.google.android.exoplayer2.ui.spherical.SingleTapListener;
|
||||
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
|
||||
|
@ -1332,14 +1333,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||
closeShutter();
|
||||
}
|
||||
|
||||
TrackSelectionArray selections = player.getCurrentTrackSelections();
|
||||
for (int i = 0; i < selections.length; i++) {
|
||||
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
|
||||
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
|
||||
// onRenderedFirstFrame().
|
||||
hideArtwork();
|
||||
return;
|
||||
}
|
||||
if (TrackSelectionUtil.hasTrackOfType(player.getCurrentTrackSelections(), C.TRACK_TYPE_VIDEO)) {
|
||||
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
|
||||
// onRenderedFirstFrame().
|
||||
hideArtwork();
|
||||
return;
|
||||
}
|
||||
|
||||
// Video disabled so the shutter must be closed.
|
||||
|
|
|
@ -436,14 +436,10 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
private StyledPlayerControlViewLayoutManager controlViewLayoutManager;
|
||||
private Resources resources;
|
||||
|
||||
private int selectedMainSettingsPosition;
|
||||
private RecyclerView settingsView;
|
||||
private SettingsAdapter settingsAdapter;
|
||||
private SubSettingsAdapter subSettingsAdapter;
|
||||
private PlaybackSpeedAdapter playbackSpeedAdapter;
|
||||
private PopupWindow settingsWindow;
|
||||
private String[] playbackSpeedTexts;
|
||||
private int[] playbackSpeedsMultBy100;
|
||||
private int selectedPlaybackSpeedIndex;
|
||||
private boolean needToHideBars;
|
||||
private int settingsWindowMargin;
|
||||
|
||||
|
@ -457,6 +453,8 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
@Nullable private ImageView fullScreenButton;
|
||||
@Nullable private ImageView minimalFullScreenButton;
|
||||
@Nullable private View settingsButton;
|
||||
@Nullable private View playbackSpeedButton;
|
||||
@Nullable private View audioTrackButton;
|
||||
|
||||
public StyledPlayerControlView(Context context) {
|
||||
this(context, /* attrs= */ null);
|
||||
|
@ -575,6 +573,16 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
settingsButton.setOnClickListener(componentListener);
|
||||
}
|
||||
|
||||
playbackSpeedButton = findViewById(R.id.exo_playback_speed);
|
||||
if (playbackSpeedButton != null) {
|
||||
playbackSpeedButton.setOnClickListener(componentListener);
|
||||
}
|
||||
|
||||
audioTrackButton = findViewById(R.id.exo_audio_track);
|
||||
if (audioTrackButton != null) {
|
||||
audioTrackButton.setOnClickListener(componentListener);
|
||||
}
|
||||
|
||||
TimeBar customTimeBar = findViewById(R.id.exo_progress);
|
||||
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
|
||||
if (customTimeBar != null) {
|
||||
|
@ -663,12 +671,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
|
||||
resources.getDrawable(R.drawable.exo_styled_controls_audiotrack);
|
||||
settingsAdapter = new SettingsAdapter(settingTexts, settingIcons);
|
||||
|
||||
playbackSpeedTexts = resources.getStringArray(R.array.exo_playback_speeds);
|
||||
playbackSpeedsMultBy100 = resources.getIntArray(R.array.exo_speed_multiplied_by_100);
|
||||
settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
|
||||
|
||||
subSettingsAdapter = new SubSettingsAdapter();
|
||||
settingsView =
|
||||
(RecyclerView)
|
||||
LayoutInflater.from(context).inflate(R.layout.exo_styled_settings_list, null);
|
||||
|
@ -693,6 +696,10 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
resources.getString(R.string.exo_controls_cc_disabled_description);
|
||||
textTrackSelectionAdapter = new TextTrackSelectionAdapter();
|
||||
audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
|
||||
playbackSpeedAdapter =
|
||||
new PlaybackSpeedAdapter(
|
||||
resources.getStringArray(R.array.exo_playback_speeds),
|
||||
resources.getIntArray(R.array.exo_speed_multiplied_by_100));
|
||||
|
||||
fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit);
|
||||
fullScreenEnterDrawable =
|
||||
|
@ -770,7 +777,6 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
this.trackSelector = null;
|
||||
}
|
||||
updateAll();
|
||||
updateSettingsPlaybackSpeedLists();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1102,6 +1108,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
updateRepeatModeButton();
|
||||
updateShuffleButton();
|
||||
updateTrackLists();
|
||||
updatePlaybackSpeedList();
|
||||
updateTimeline();
|
||||
}
|
||||
|
||||
|
@ -1141,11 +1148,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
timeline.getWindow(player.getCurrentWindowIndex(), window);
|
||||
boolean isSeekable = window.isSeekable;
|
||||
enableSeeking = isSeekable;
|
||||
enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();
|
||||
enablePrevious = isSeekable || !window.isLive() || player.hasPrevious();
|
||||
enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
|
||||
enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
|
||||
enableNext =
|
||||
window.isLive() || player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
|
||||
(window.isLive() && window.isDynamic)
|
||||
|| player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1438,24 +1446,13 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateSettingsPlaybackSpeedLists() {
|
||||
private void updatePlaybackSpeedList() {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
float speed = player.getPlaybackParameters().speed;
|
||||
int currentSpeedMultBy100 = Math.round(speed * 100);
|
||||
int closestMatchIndex = 0;
|
||||
int closestMatchDifference = Integer.MAX_VALUE;
|
||||
for (int i = 0; i < playbackSpeedsMultBy100.length; i++) {
|
||||
int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]);
|
||||
if (difference < closestMatchDifference) {
|
||||
closestMatchIndex = i;
|
||||
closestMatchDifference = difference;
|
||||
}
|
||||
}
|
||||
selectedPlaybackSpeedIndex = closestMatchIndex;
|
||||
playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed);
|
||||
settingsAdapter.setSubTextAtPosition(
|
||||
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedTexts[closestMatchIndex]);
|
||||
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText());
|
||||
}
|
||||
|
||||
private void updateSettingsWindowSize() {
|
||||
|
@ -1571,27 +1568,14 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
|
||||
private void onSettingViewClicked(int position) {
|
||||
if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
|
||||
subSettingsAdapter.init(playbackSpeedTexts, selectedPlaybackSpeedIndex);
|
||||
selectedMainSettingsPosition = SETTINGS_PLAYBACK_SPEED_POSITION;
|
||||
displaySettingsWindow(subSettingsAdapter);
|
||||
displaySettingsWindow(playbackSpeedAdapter);
|
||||
} else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
|
||||
selectedMainSettingsPosition = SETTINGS_AUDIO_TRACK_SELECTION_POSITION;
|
||||
displaySettingsWindow(audioTrackSelectionAdapter);
|
||||
} else {
|
||||
settingsWindow.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void onSubSettingViewClicked(int position) {
|
||||
if (selectedMainSettingsPosition == SETTINGS_PLAYBACK_SPEED_POSITION) {
|
||||
if (position != selectedPlaybackSpeedIndex) {
|
||||
float speed = playbackSpeedsMultBy100[position] / 100.0f;
|
||||
setPlaybackSpeed(speed);
|
||||
}
|
||||
}
|
||||
settingsWindow.dismiss();
|
||||
}
|
||||
|
||||
private void onLayoutChange(
|
||||
View v,
|
||||
int left,
|
||||
|
@ -1836,7 +1820,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
updateTimeline();
|
||||
}
|
||||
if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
||||
updateSettingsPlaybackSpeedLists();
|
||||
updatePlaybackSpeedList();
|
||||
}
|
||||
if (events.contains(EVENT_TRACKS_CHANGED)) {
|
||||
updateTrackLists();
|
||||
|
@ -1877,6 +1861,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
} else if (settingsButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(settingsAdapter);
|
||||
} else if (playbackSpeedButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(playbackSpeedAdapter);
|
||||
} else if (audioTrackButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(audioTrackSelectionAdapter);
|
||||
} else if (subtitleButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(textTrackSelectionAdapter);
|
||||
|
@ -1950,18 +1940,33 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private class SubSettingsAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
||||
private final class PlaybackSpeedAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
||||
|
||||
private String[] texts;
|
||||
private final String[] playbackSpeedTexts;
|
||||
private final int[] playbackSpeedsMultBy100;
|
||||
private int selectedIndex;
|
||||
|
||||
public SubSettingsAdapter() {
|
||||
texts = new String[0];
|
||||
public PlaybackSpeedAdapter(String[] playbackSpeedTexts, int[] playbackSpeedsMultBy100) {
|
||||
this.playbackSpeedTexts = playbackSpeedTexts;
|
||||
this.playbackSpeedsMultBy100 = playbackSpeedsMultBy100;
|
||||
}
|
||||
|
||||
public void init(String[] texts, int selectedIndex) {
|
||||
this.texts = texts;
|
||||
this.selectedIndex = selectedIndex;
|
||||
public void updateSelectedIndex(float playbackSpeed) {
|
||||
int currentSpeedMultBy100 = Math.round(playbackSpeed * 100);
|
||||
int closestMatchIndex = 0;
|
||||
int closestMatchDifference = Integer.MAX_VALUE;
|
||||
for (int i = 0; i < playbackSpeedsMultBy100.length; i++) {
|
||||
int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]);
|
||||
if (difference < closestMatchDifference) {
|
||||
closestMatchIndex = i;
|
||||
closestMatchDifference = difference;
|
||||
}
|
||||
}
|
||||
selectedIndex = closestMatchIndex;
|
||||
}
|
||||
|
||||
public String getSelectedText() {
|
||||
return playbackSpeedTexts[selectedIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1974,27 +1979,23 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||
if (position < texts.length) {
|
||||
holder.textView.setText(texts[position]);
|
||||
if (position < playbackSpeedTexts.length) {
|
||||
holder.textView.setText(playbackSpeedTexts[position]);
|
||||
}
|
||||
holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
|
||||
holder.itemView.setOnClickListener(
|
||||
v -> {
|
||||
if (position != selectedIndex) {
|
||||
float speed = playbackSpeedsMultBy100[position] / 100.0f;
|
||||
setPlaybackSpeed(speed);
|
||||
}
|
||||
settingsWindow.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return texts.length;
|
||||
}
|
||||
}
|
||||
|
||||
private final class SubSettingViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView textView;
|
||||
private final View checkView;
|
||||
|
||||
public SubSettingViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.exo_text);
|
||||
checkView = itemView.findViewById(R.id.exo_check);
|
||||
itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition()));
|
||||
return playbackSpeedTexts.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2042,7 +2043,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
|
||||
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
|
||||
// CC options include "Off" at the first position, which disables text rendering.
|
||||
holder.textView.setText(R.string.exo_track_selection_none);
|
||||
boolean isTrackSelectionOff = true;
|
||||
|
@ -2071,7 +2072,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
|
||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
if (position > 0) {
|
||||
TrackInfo track = tracks.get(position - 1);
|
||||
|
@ -2088,7 +2089,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
|
||||
|
||||
@Override
|
||||
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
|
||||
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
|
||||
// Audio track selection option includes "Auto" at the top.
|
||||
holder.textView.setText(R.string.exo_track_selection_auto);
|
||||
// hasSelectionOverride is true means there is an explicit track selection, not "Auto".
|
||||
|
@ -2167,8 +2168,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private abstract class TrackSelectionAdapter
|
||||
extends RecyclerView.Adapter<TrackSelectionViewHolder> {
|
||||
private abstract class TrackSelectionAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
||||
|
||||
protected List<Integer> rendererIndices;
|
||||
protected List<TrackInfo> tracks;
|
||||
|
@ -2184,19 +2184,19 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
List<Integer> rendererIndices, List<TrackInfo> trackInfos, MappedTrackInfo mappedTrackInfo);
|
||||
|
||||
@Override
|
||||
public TrackSelectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v =
|
||||
LayoutInflater.from(getContext())
|
||||
.inflate(R.layout.exo_styled_sub_settings_list_item, null);
|
||||
return new TrackSelectionViewHolder(v);
|
||||
return new SubSettingViewHolder(v);
|
||||
}
|
||||
|
||||
public abstract void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder);
|
||||
public abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder);
|
||||
|
||||
public abstract void onTrackSelection(String subtext);
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
|
||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||
if (trackSelector == null || mappedTrackInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -2252,12 +2252,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private static class TrackSelectionViewHolder extends RecyclerView.ViewHolder {
|
||||
private static class SubSettingViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public final TextView textView;
|
||||
public final View checkView;
|
||||
|
||||
public TrackSelectionViewHolder(View itemView) {
|
||||
public SubSettingViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.exo_text);
|
||||
checkView = itemView.findViewById(R.id.exo_check);
|
||||
|
|
|
@ -607,7 +607,7 @@ import java.util.List;
|
|||
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true);
|
||||
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
|
||||
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false);
|
||||
} else if (uxState != UX_STATE_ANIMATING_HIDE && uxState != UX_STATE_ANIMATING_SHOW) {
|
||||
} else if (uxState != UX_STATE_ANIMATING_HIDE) {
|
||||
defaultTimeBar.showScrubber();
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue