Commit d80d5485 by Ian Baker Committed by GitHub

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

parents 8ac74a00 6be3a593
Showing with 1483 additions and 717 deletions
......@@ -47,6 +47,7 @@ bazel-testlogs
.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"/>
......
......@@ -485,26 +485,6 @@ public final class CastPlayer extends BasePlayer {
}
@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) {
return;
......@@ -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));
// Fill buffer with more data from Cronet.
operation.close();
try {
readInternal(buffer);
} 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.
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();
}
}
if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
}
final int bytesRead = readLength - buffer.remaining();
// The operation didn't time out, fail or finish, and therefore data must have been read.
Assertions.checkState(readLength > buffer.remaining());
int bytesRead = readLength - buffer.remaining();
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
......@@ -886,12 +861,48 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws IOException If an error occurs reading from the source.
* @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
* specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
*/
private boolean skipFully(long bytesToSkip) throws IOException {
if (bytesToSkip == 0) {
return true;
}
ByteBuffer readBuffer = getOrCreateReadBuffer();
while (bytesToSkip > 0) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
readInternal(readBuffer);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (finished) {
return false;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
readBuffer.position(readBuffer.position() + bytesSkipped);
bytesToSkip -= bytesSkipped;
}
}
return true;
}
/**
* Reads the whole response body.
*
* @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
......
......@@ -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);
}
......@@ -138,6 +138,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;
......
......@@ -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();
}
}
}
/*
* 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);
}
......
......@@ -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();
......
......@@ -1294,13 +1294,6 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public void setMediaItems(List<MediaItem> mediaItems) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
player.setMediaItems(mediaItems);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
verifyApplicationThread();
analyticsCollector.resetForNewPlaylist();
......@@ -1316,27 +1309,6 @@ public class SimpleExoPlayer extends BasePlayer
}
@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();
analyticsCollector.resetForNewPlaylist();
......@@ -1392,18 +1364,6 @@ public class SimpleExoPlayer extends BasePlayer
}
@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();
player.addMediaSource(mediaSource);
......@@ -1428,24 +1388,12 @@ public class SimpleExoPlayer extends BasePlayer
}
@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();
player.removeMediaItems(fromIndex, toIndex);
......@@ -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,9 +754,19 @@ 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,9 +906,19 @@ 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) {
......
......@@ -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() {
......@@ -82,6 +99,51 @@ public interface DrmSessionManager {
}
/**
* Pre-acquires a DRM session for the specified {@link Format}.
*
* <p>This notifies the manager that a subsequent call to {@link #acquireSession(Looper,
* DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely,
* allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready
* in the background.
*
* <p>The caller must call {@link DrmSessionReference#release()} on the returned instance when
* they no longer require the pre-acquisition (i.e. they know they won't be making a matching call
* to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near
* future).
*
* <p>This manager may silently release the underlying session in order to allow another operation
* to complete. This will result in a subsequent call to {@link #acquireSession(Looper,
* DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including
* repeating key loads and other async initialization steps.
*
* <p>The caller must separately call {@link #acquireSession(Looper,
* DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for
* playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances
* are distinct. The caller must release both, and can release the {@link DrmSessionReference}
* before the {@link DrmSession} without affecting playback.
*
* <p>This can be called from any thread.
*
* <p>Implementations that do not support pre-acquisition always return an empty {@link
* DrmSessionReference} instance.
*
* @param playbackLooper The looper associated with the media playback thread.
* @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
* events, and passed on to {@link
* DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
* @param format The {@link Format} for which to pre-acquire a {@link DrmSession}.
* @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching
* {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would
* return null.
*/
default DrmSessionReference preacquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
return DrmSessionReference.EMPTY;
}
/**
* Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is
* not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs
......
......@@ -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
......@@ -1693,6 +1689,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* 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.
*
* @param format The {@link Format} for which the operating rate should be configured.
* @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.
......@@ -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;
......@@ -73,14 +73,25 @@ public final class ProgressiveMediaSource extends BaseMediaSource
}
/**
* Equivalent to {@link #Factory(DataSource.Factory, ProgressiveMediaExtractor.Factory) new
* Factory(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory)}.
*/
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this(dataSourceFactory, () -> new BundledExtractorsAdapter(extractorsFactory));
}
/**
* Creates a new factory for {@link ProgressiveMediaSource}s.
*
* @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);
});
}
}
......
......@@ -8606,6 +8606,20 @@ public final class ExoPlayerTest {
}
@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 {
long windowStartUnixTimeMs = 987_654_321_000L;
......
......@@ -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());
}
......
......@@ -108,18 +108,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 {
schemeDataDataSource.open(buildDataSpec("http://www.google.com"));
......
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.ResolvingDataSource.Resolver;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import org.junit.Before;
import org.junit.runner.RunWith;
/** {@link DataSource} contract tests for {@link ResolvingDataSourceContractTest}. */
@RunWith(AndroidJUnit4.class)
public class ResolvingDataSourceContractTest extends DataSourceContractTest {
private static final String URI = "test://simple.test";
private static final String RESOLVED_URI = "resolved://simple.resolved";
private byte[] simpleData;
private FakeDataSet fakeDataSet;
private FakeDataSource fakeDataSource;
@Before
public void setUp() {
simpleData = TestUtil.buildTestData(/* length= */ 20);
fakeDataSet = new FakeDataSet().newData(RESOLVED_URI).appendReadData(simpleData).endData();
}
@Override
protected ImmutableList<TestResource> getTestResources() {
return ImmutableList.of(
new TestResource.Builder()
.setName("simple")
.setUri(URI)
.setExpectedBytes(simpleData)
.build());
}
@Override
protected Uri getNotFoundUri() {
return Uri.parse("test://not-found.test");
}
@Override
protected DataSource createDataSource() {
fakeDataSource = new FakeDataSource(fakeDataSet);
return new ResolvingDataSource(
fakeDataSource,
new Resolver() {
@Override
public DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException {
return URI.equals(dataSpec.uri.toString())
? dataSpec.buildUpon().setUri(RESOLVED_URI).build()
: dataSpec;
}
});
}
@Override
@Nullable
protected DataSource getTransferListenerDataSource() {
return fakeDataSource;
}
}
......@@ -54,13 +54,17 @@ public class UdpDataSourceContractTest extends DataSourceContractTest {
}
@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);
......@@ -264,6 +221,50 @@ public final class CacheWriterTest {
}
@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();
FakeDataSet fakeDataSet =
......
......@@ -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());
}
}
......@@ -666,26 +664,6 @@ public class DefaultDashChunkSource implements DashChunkSource {
/* 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,
long segmentNumShift,
......@@ -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.
......
......@@ -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();
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment