playlistMediaItems =
+ ImmutableList.of(createMediaItem(0), createMediaItem(1));
+ MediaQueueItem[] initialPlaylistTwoQueueItems =
+ new MediaQueueItem[] {
+ createMediaQueueItem(playlistMediaItems.get(0), 0),
+ createMediaQueueItem(playlistMediaItems.get(1), 1)
+ };
+ castTimelineTracker.onMediaItemsSet(playlistMediaItems, initialPlaylistTwoQueueItems);
+ when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
+ when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
+ // Mock remote media client state with two items in the queue.
+ when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1});
+ when(mockMediaStatus.getCurrentItemId()).thenReturn(0);
+ when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[0].getMedia());
+ when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(initialPlaylistTwoQueueItems));
+
+ CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
+
+ assertThat(castTimeline.getWindowCount()).isEqualTo(2);
+ assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(2);
+
+ // Mock remote media client state after the first item has been removed.
+ when(mockMediaQueue.getItemIds()).thenReturn(new int[] {1});
+ when(mockMediaStatus.getCurrentItemId()).thenReturn(1);
+ when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[1].getMedia());
+ when(mockMediaStatus.getQueueItems())
+ .thenReturn(ImmutableList.of(initialPlaylistTwoQueueItems[1]));
+
+ castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
+
+ assertThat(castTimeline.getWindowCount()).isEqualTo(1);
+ assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem)
+ .isEqualTo(playlistMediaItems.get(1));
+ // Assert that the removed item has been removed from the content ID map.
+ assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1);
+
+ // Mock remote media client state for empty queue.
+ when(mockRemoteMediaClient.getMediaStatus()).thenReturn(null);
+ when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
+ when(mockMediaStatus.getCurrentItemId()).thenReturn(MediaQueueItem.INVALID_ITEM_ID);
+ when(mockMediaStatus.getMediaInfo()).thenReturn(null);
+ when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of());
+
+ castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient);
+
+ assertThat(castTimeline.getWindowCount()).isEqualTo(0);
+ // Queue is not emptied when remote media client is empty. See [Internal ref: b/128825216].
+ assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1);
+ }
+
+ private MediaItem createMediaItem(int uid) {
+ return new MediaItem.Builder()
+ .setUri("http://www.google.com/" + uid)
+ .setMimeType(MimeTypes.AUDIO_MPEG)
+ .setTag(uid)
+ .build();
+ }
+
+ private MediaQueueItem createMediaQueueItem(MediaItem mediaItem, int uid) {
+ return new MediaQueueItem.Builder(mediaItemConverter.toMediaQueueItem(mediaItem))
+ .setItemId(uid)
+ .build();
+ }
+
private static RemoteMediaClient mockRemoteMediaClient(
int[] itemIds, int currentItemId, long currentDurationMs) {
- RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
- MediaStatus status = Mockito.mock(MediaStatus.class);
+ RemoteMediaClient remoteMediaClient = mock(RemoteMediaClient.class);
+ MediaStatus status = mock(MediaStatus.class);
when(status.getQueueItems()).thenReturn(Collections.emptyList());
when(remoteMediaClient.getMediaStatus()).thenReturn(status);
when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
@@ -118,7 +305,7 @@ private static RemoteMediaClient mockRemoteMediaClient(
}
private static MediaQueue mockMediaQueue(int[] itemIds) {
- MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
+ MediaQueue mediaQueue = mock(MediaQueue.class);
when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue;
}
diff --git a/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java b/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java
index 0a760043d37..10ac47a62ea 100644
--- a/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java
+++ b/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java
@@ -50,6 +50,7 @@ public void serialize_deserialize_complete() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item =
builder
+ .setMediaId("fooBar")
.setUri(Uri.parse("http://example.com"))
.setMediaMetadata(MediaMetadata.EMPTY)
.setMimeType(MimeTypes.APPLICATION_MPD)
@@ -66,4 +67,45 @@ public void serialize_deserialize_complete() {
assertThat(reconstructedItem).isEqualTo(item);
}
+
+ @Test
+ public void toMediaQueueItem_nonDefaultMediaId_usedAsContentId() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item =
+ builder
+ .setMediaId("fooBar")
+ .setUri("http://example.com")
+ .setMimeType(MimeTypes.APPLICATION_MPD)
+ .build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+
+ assertThat(queueItem.getMedia().getContentId()).isEqualTo("fooBar");
+ }
+
+ @Test
+ public void toMediaQueueItem_defaultMediaId_uriAsContentId() {
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaItem mediaItem =
+ new MediaItem.Builder()
+ .setUri("http://example.com")
+ .setMimeType(MimeTypes.APPLICATION_MPD)
+ .build();
+
+ MediaQueueItem queueItem = converter.toMediaQueueItem(mediaItem);
+
+ assertThat(queueItem.getMedia().getContentId()).isEqualTo("http://example.com");
+
+ MediaItem secondMediaItem =
+ new MediaItem.Builder()
+ .setMediaId(MediaItem.DEFAULT_MEDIA_ID)
+ .setUri("http://example.com")
+ .setMimeType(MimeTypes.APPLICATION_MPD)
+ .build();
+
+ MediaQueueItem secondQueueItem = converter.toMediaQueueItem(secondMediaItem);
+
+ assertThat(secondQueueItem.getMedia().getContentId()).isEqualTo("http://example.com");
+ }
}
diff --git a/libraries/common/build.gradle b/libraries/common/build.gradle
index 048fe60f41b..85169e2ec8b 100644
--- a/libraries/common/build.gradle
+++ b/libraries/common/build.gradle
@@ -75,6 +75,7 @@ dependencies {
testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ testImplementation project(modulePrefix + 'lib-exoplayer')
testImplementation project(modulePrefix + 'test-utils')
}
diff --git a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java
index 7a971ef9802..65fe0958289 100644
--- a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java
+++ b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java
@@ -18,6 +18,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.util.Assertions;
@@ -157,6 +158,7 @@ public SchemeData get(int index) {
* @param schemeType A protection scheme type. May be null.
* @return A copy with the specified protection scheme type.
*/
+ @CheckResult
public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
if (Util.areEqual(this.schemeType, schemeType)) {
return this;
@@ -333,6 +335,7 @@ public boolean hasData() {
* @param data The data to include in the copy.
* @return The new instance.
*/
+ @CheckResult
public SchemeData copyWithData(@Nullable byte[] data) {
return new SchemeData(uuid, licenseServerUrl, mimeType, data);
}
diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java
index 4e87f65806c..62be209a9b8 100644
--- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java
+++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java
@@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "1.0.0-beta01";
+ public static final String VERSION = "1.0.0-beta02";
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta01";
+ public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta02";
/**
* The version of the library expressed as an integer, for example 1002003300.
@@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
* (123-045-006-3-00).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 1_000_000_1_01;
+ public static final int VERSION_INT = 1_000_000_1_02;
/** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true;
diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java
index 8cd90d2da18..4f2834b1ef2 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Player.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Player.java
@@ -373,6 +373,7 @@ public static final class Builder {
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
+ COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
@@ -384,7 +385,6 @@ public static final class Builder {
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
- COMMAND_SET_MEDIA_ITEM,
};
private final FlagSet.Builder flagsBuilder;
@@ -1432,6 +1432,7 @@ default void onMetadata(Metadata metadata) {}
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
+ COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
@@ -1443,7 +1444,6 @@ default void onMetadata(Metadata metadata) {}
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
- COMMAND_SET_MEDIA_ITEM,
})
@interface Command {}
/** Command to start, pause or resume playback. */
@@ -1501,6 +1501,8 @@ default void onMetadata(Metadata metadata) {}
int COMMAND_GET_MEDIA_ITEMS_METADATA = 18;
/** Command to set the {@link MediaItem MediaItems} metadata. */
int COMMAND_SET_MEDIA_ITEMS_METADATA = 19;
+ /** Command to set a {@link MediaItem MediaItem}. */
+ int COMMAND_SET_MEDIA_ITEM = 31;
/** Command to change the {@link MediaItem MediaItems} in the playlist. */
int COMMAND_CHANGE_MEDIA_ITEMS = 20;
/** Command to get the player current {@link AudioAttributes}. */
@@ -1523,8 +1525,6 @@ default void onMetadata(Metadata metadata) {}
int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29;
/** Command to get details of the current track selection. */
int COMMAND_GET_TRACKS = 30;
- /** Command to set a {@link MediaItem MediaItem}. */
- int COMMAND_SET_MEDIA_ITEM = 31;
/** Represents an invalid {@link Command}. */
int COMMAND_INVALID = -1;
diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java
index 3f665f8bfc8..14b04065f50 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java
@@ -1351,6 +1351,27 @@ public boolean equals(@Nullable Object obj) {
return false;
}
}
+
+ // Check shuffled order
+ int windowIndex = getFirstWindowIndex(/* shuffleModeEnabled= */ true);
+ if (windowIndex != other.getFirstWindowIndex(/* shuffleModeEnabled= */ true)) {
+ return false;
+ }
+ int lastWindowIndex = getLastWindowIndex(/* shuffleModeEnabled= */ true);
+ if (lastWindowIndex != other.getLastWindowIndex(/* shuffleModeEnabled= */ true)) {
+ return false;
+ }
+ while (windowIndex != lastWindowIndex) {
+ int nextWindowIndex =
+ getNextWindowIndex(windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true);
+ if (nextWindowIndex
+ != other.getNextWindowIndex(
+ windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) {
+ return false;
+ }
+ windowIndex = nextWindowIndex;
+ }
+
return true;
}
@@ -1367,6 +1388,13 @@ public int hashCode() {
for (int i = 0; i < getPeriodCount(); i++) {
result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode();
}
+
+ for (int windowIndex = getFirstWindowIndex(true);
+ windowIndex != C.INDEX_UNSET;
+ windowIndex = getNextWindowIndex(windowIndex, Player.REPEAT_MODE_OFF, true)) {
+ result = 31 * result + windowIndex;
+ }
+
return result;
}
diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Log.java b/libraries/common/src/main/java/androidx/media3/common/util/Log.java
index ce0d25dfadc..b1a97f77fa0 100644
--- a/libraries/common/src/main/java/androidx/media3/common/util/Log.java
+++ b/libraries/common/src/main/java/androidx/media3/common/util/Log.java
@@ -18,6 +18,7 @@
import static java.lang.annotation.ElementType.TYPE_USE;
import android.text.TextUtils;
+import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
@@ -28,7 +29,10 @@
import java.net.UnknownHostException;
import org.checkerframework.dataflow.qual.Pure;
-/** Wrapper around {@link android.util.Log} which allows to set the log level. */
+/**
+ * Wrapper around {@link android.util.Log} which allows to set the log level and to specify a custom
+ * log output.
+ */
@UnstableApi
public final class Log {
@@ -52,15 +56,89 @@ public final class Log {
/** Log level to disable all logging. */
public static final int LOG_LEVEL_OFF = Integer.MAX_VALUE;
+ /**
+ * Interface for a logger that can output messages with a tag.
+ *
+ * Use {@link #DEFAULT} to output to {@link android.util.Log}.
+ */
+ public interface Logger {
+
+ /** The default instance logging to {@link android.util.Log}. */
+ Logger DEFAULT =
+ new Logger() {
+ @Override
+ public void d(String tag, String message) {
+ android.util.Log.d(tag, message);
+ }
+
+ @Override
+ public void i(String tag, String message) {
+ android.util.Log.i(tag, message);
+ }
+
+ @Override
+ public void w(String tag, String message) {
+ android.util.Log.w(tag, message);
+ }
+
+ @Override
+ public void e(String tag, String message) {
+ android.util.Log.e(tag, message);
+ }
+ };
+
+ /**
+ * Logs a debug-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void d(String tag, String message);
+
+ /**
+ * Logs an information-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void i(String tag, String message);
+
+ /**
+ * Logs a warning-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void w(String tag, String message);
+
+ /**
+ * Logs an error-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void e(String tag, String message);
+ }
+
+ private static final Object lock = new Object();
+
+ @GuardedBy("lock")
private static int logLevel = LOG_LEVEL_ALL;
+
+ @GuardedBy("lock")
private static boolean logStackTraces = true;
+ @GuardedBy("lock")
+ private static Logger logger = Logger.DEFAULT;
+
private Log() {}
/** Returns current {@link LogLevel} for ExoPlayer logcat logging. */
@Pure
public static @LogLevel int getLogLevel() {
- return logLevel;
+ synchronized (lock) {
+ return logLevel;
+ }
}
/**
@@ -69,7 +147,9 @@ private Log() {}
* @param logLevel The new {@link LogLevel}.
*/
public static void setLogLevel(@LogLevel int logLevel) {
- Log.logLevel = logLevel;
+ synchronized (lock) {
+ Log.logLevel = logLevel;
+ }
}
/**
@@ -79,7 +159,20 @@ public static void setLogLevel(@LogLevel int logLevel) {
* @param logStackTraces Whether stack traces will be logged.
*/
public static void setLogStackTraces(boolean logStackTraces) {
- Log.logStackTraces = logStackTraces;
+ synchronized (lock) {
+ Log.logStackTraces = logStackTraces;
+ }
+ }
+
+ /**
+ * Sets a custom {@link Logger} as the output.
+ *
+ * @param logger The {@link Logger}.
+ */
+ public static void setLogger(Logger logger) {
+ synchronized (lock) {
+ Log.logger = logger;
+ }
}
/**
@@ -87,8 +180,10 @@ public static void setLogStackTraces(boolean logStackTraces) {
*/
@Pure
public static void d(@Size(max = 23) String tag, String message) {
- if (logLevel == LOG_LEVEL_ALL) {
- android.util.Log.d(tag, message);
+ synchronized (lock) {
+ if (logLevel == LOG_LEVEL_ALL) {
+ logger.d(tag, message);
+ }
}
}
@@ -105,8 +200,10 @@ public static void d(@Size(max = 23) String tag, String message, @Nullable Throw
*/
@Pure
public static void i(@Size(max = 23) String tag, String message) {
- if (logLevel <= LOG_LEVEL_INFO) {
- android.util.Log.i(tag, message);
+ synchronized (lock) {
+ if (logLevel <= LOG_LEVEL_INFO) {
+ logger.i(tag, message);
+ }
}
}
@@ -123,8 +220,10 @@ public static void i(@Size(max = 23) String tag, String message, @Nullable Throw
*/
@Pure
public static void w(@Size(max = 23) String tag, String message) {
- if (logLevel <= LOG_LEVEL_WARNING) {
- android.util.Log.w(tag, message);
+ synchronized (lock) {
+ if (logLevel <= LOG_LEVEL_WARNING) {
+ logger.w(tag, message);
+ }
}
}
@@ -141,8 +240,10 @@ public static void w(@Size(max = 23) String tag, String message, @Nullable Throw
*/
@Pure
public static void e(@Size(max = 23) String tag, String message) {
- if (logLevel <= LOG_LEVEL_ERROR) {
- android.util.Log.e(tag, message);
+ synchronized (lock) {
+ if (logLevel <= LOG_LEVEL_ERROR) {
+ logger.e(tag, message);
+ }
}
}
@@ -168,20 +269,23 @@ public static void e(@Size(max = 23) String tag, String message, @Nullable Throw
@Nullable
@Pure
public static String getThrowableString(@Nullable Throwable throwable) {
- if (throwable == null) {
- return null;
- } else if (isCausedByUnknownHostException(throwable)) {
- // UnknownHostException implies the device doesn't have network connectivity.
- // UnknownHostException.getMessage() may return a string that's more verbose than desired for
- // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
- // special handling to return the empty string, which can result in logging that doesn't
- // indicate the failure mode at all. Hence we special case this exception to always return a
- // concise but useful message.
- return "UnknownHostException (no network)";
- } else if (!logStackTraces) {
- return throwable.getMessage();
- } else {
- return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
+ synchronized (lock) {
+ if (throwable == null) {
+ return null;
+ } else if (isCausedByUnknownHostException(throwable)) {
+ // UnknownHostException implies the device doesn't have network connectivity.
+ // UnknownHostException.getMessage() may return a string that's more verbose than desired
+ // for
+ // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
+ // special handling to return the empty string, which can result in logging that doesn't
+ // indicate the failure mode at all. Hence we special case this exception to always return a
+ // concise but useful message.
+ return "UnknownHostException (no network)";
+ } else if (!logStackTraces) {
+ return throwable.getMessage();
+ } else {
+ return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
+ }
}
}
diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java
index 7238fad0fe8..7ea6c3d1f86 100644
--- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java
+++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java
@@ -55,6 +55,7 @@
import android.os.Looper;
import android.os.Parcel;
import android.os.SystemClock;
+import android.provider.MediaStore;
import android.security.NetworkSecurityPolicy;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -199,7 +200,7 @@ public static byte[] toByteArray(InputStream inputStream) throws IOException {
@UnstableApi
@Nullable
public static ComponentName startForegroundService(Context context, Intent intent) {
- if (Util.SDK_INT >= 26) {
+ if (SDK_INT >= 26) {
return context.startForegroundService(intent);
} else {
return context.startService(intent);
@@ -215,12 +216,12 @@ public static ComponentName startForegroundService(Context context, Intent inten
* @return Whether a permission request was made.
*/
public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
- if (Util.SDK_INT < 23) {
+ if (SDK_INT < 23) {
return false;
}
for (Uri uri : uris) {
- if (isLocalFileUri(uri)) {
- return requestExternalStoragePermission(activity);
+ if (maybeRequestReadExternalStoragePermission(activity, uri)) {
+ return true;
}
}
return false;
@@ -238,25 +239,46 @@ public static boolean maybeRequestReadExternalStoragePermission(Activity activit
*/
public static boolean maybeRequestReadExternalStoragePermission(
Activity activity, MediaItem... mediaItems) {
- if (Util.SDK_INT < 23) {
+ if (SDK_INT < 23) {
return false;
}
for (MediaItem mediaItem : mediaItems) {
if (mediaItem.localConfiguration == null) {
continue;
}
- if (isLocalFileUri(mediaItem.localConfiguration.uri)) {
- return requestExternalStoragePermission(activity);
+ if (maybeRequestReadExternalStoragePermission(activity, mediaItem.localConfiguration.uri)) {
+ return true;
}
- for (int i = 0; i < mediaItem.localConfiguration.subtitleConfigurations.size(); i++) {
- if (isLocalFileUri(mediaItem.localConfiguration.subtitleConfigurations.get(i).uri)) {
- return requestExternalStoragePermission(activity);
+ List subtitleConfigs =
+ mediaItem.localConfiguration.subtitleConfigurations;
+ for (int i = 0; i < subtitleConfigs.size(); i++) {
+ if (maybeRequestReadExternalStoragePermission(activity, subtitleConfigs.get(i).uri)) {
+ return true;
}
}
}
return false;
}
+ private static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri uri) {
+ return SDK_INT >= 23
+ && (isLocalFileUri(uri) || isMediaStoreExternalContentUri(uri))
+ && requestExternalStoragePermission(activity);
+ }
+
+ private static boolean isMediaStoreExternalContentUri(Uri uri) {
+ if (!"content".equals(uri.getScheme()) || !MediaStore.AUTHORITY.equals(uri.getAuthority())) {
+ return false;
+ }
+ List pathSegments = uri.getPathSegments();
+ if (pathSegments.isEmpty()) {
+ return false;
+ }
+ String firstPathSegment = pathSegments.get(0);
+ return MediaStore.VOLUME_EXTERNAL.equals(firstPathSegment)
+ || MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(firstPathSegment);
+ }
+
/**
* Returns whether it may be possible to load the URIs of the given media items based on the
* network security policy's cleartext traffic permissions.
@@ -265,7 +287,7 @@ public static boolean maybeRequestReadExternalStoragePermission(
* @return Whether it may be possible to load the URIs of the given media items.
*/
public static boolean checkCleartextTrafficPermitted(MediaItem... mediaItems) {
- if (Util.SDK_INT < 24) {
+ if (SDK_INT < 24) {
// We assume cleartext traffic is permitted.
return true;
}
@@ -650,7 +672,7 @@ public static String getLocaleLanguageTag(Locale locale) {
normalizedTag = language;
}
normalizedTag = Ascii.toLowerCase(normalizedTag);
- String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0];
+ String mainLanguage = splitAtFirst(normalizedTag, "-")[0];
if (languageTagReplacementMap == null) {
languageTagReplacementMap = createIsoLanguageReplacementMap();
}
@@ -1712,9 +1734,9 @@ public static int getAudioTrackChannelConfig(int channelCount) {
case 7:
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
case 8:
- if (Util.SDK_INT >= 23) {
+ if (SDK_INT >= 23) {
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
- } else if (Util.SDK_INT >= 21) {
+ } else if (SDK_INT >= 21) {
// Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.
return AudioFormat.CHANNEL_OUT_5POINT1
| AudioFormat.CHANNEL_OUT_SIDE_LEFT
@@ -2005,7 +2027,7 @@ public static UUID getDrmUuid(String drmScheme) {
public static @ContentType int inferContentTypeForUriAndMimeType(
Uri uri, @Nullable String mimeType) {
if (mimeType == null) {
- return Util.inferContentType(uri);
+ return inferContentType(uri);
}
switch (mimeType) {
case MimeTypes.APPLICATION_MPD:
@@ -2345,7 +2367,7 @@ public static String[] getSystemLanguageCodes() {
/** Returns the default {@link Locale.Category#DISPLAY DISPLAY} {@link Locale}. */
@UnstableApi
public static Locale getDefaultDisplayLocale() {
- return Util.SDK_INT >= 24 ? Locale.getDefault(Locale.Category.DISPLAY) : Locale.getDefault();
+ return SDK_INT >= 24 ? Locale.getDefault(Locale.Category.DISPLAY) : Locale.getDefault();
}
/**
@@ -2420,7 +2442,7 @@ public static boolean isTv(Context context) {
*/
@UnstableApi
public static boolean isAutomotive(Context context) {
- return Util.SDK_INT >= 23
+ return SDK_INT >= 23
&& context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
@@ -2439,7 +2461,7 @@ public static boolean isAutomotive(Context context) {
@UnstableApi
public static Point getCurrentDisplayModeSize(Context context) {
@Nullable Display defaultDisplay = null;
- if (Util.SDK_INT >= 17) {
+ if (SDK_INT >= 17) {
@Nullable
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
@@ -2488,7 +2510,7 @@ public static Point getCurrentDisplayModeSize(Context context, Display display)
// vendor.display-size instead.
@Nullable
String displaySize =
- Util.SDK_INT < 28
+ SDK_INT < 28
? getSystemProperty("sys.display-size")
: getSystemProperty("vendor.display-size");
// If we managed to read the display size, attempt to parse it.
@@ -2509,17 +2531,17 @@ public static Point getCurrentDisplayModeSize(Context context, Display display)
}
// Sony Android TVs advertise support for 4k output via a system feature.
- if ("Sony".equals(Util.MANUFACTURER)
- && Util.MODEL.startsWith("BRAVIA")
+ if ("Sony".equals(MANUFACTURER)
+ && MODEL.startsWith("BRAVIA")
&& context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
return new Point(3840, 2160);
}
}
Point displaySize = new Point();
- if (Util.SDK_INT >= 23) {
+ if (SDK_INT >= 23) {
getDisplaySizeV23(display, displaySize);
- } else if (Util.SDK_INT >= 17) {
+ } else if (SDK_INT >= 17) {
getDisplaySizeV17(display, displaySize);
} else {
getDisplaySizeV16(display, displaySize);
@@ -2745,7 +2767,7 @@ private static String[] getSystemLocales() {
@RequiresApi(24)
private static String[] getSystemLocalesV24(Configuration config) {
- return Util.split(config.getLocales().toLanguageTags(), ",");
+ return split(config.getLocales().toLanguageTags(), ",");
}
@RequiresApi(21)
diff --git a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java
index f2de7511930..6844330e14f 100644
--- a/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java
+++ b/libraries/common/src/test/java/androidx/media3/common/TimelineTest.java
@@ -19,6 +19,7 @@
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem.LiveConfiguration;
+import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.TimelineAsserts;
@@ -64,6 +65,50 @@ public void multiPeriodTimeline() {
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
}
+ @Test
+ public void timelineEquals() {
+ ImmutableList timelineWindowDefinitions =
+ ImmutableList.of(
+ new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 111),
+ new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 222),
+ new TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 333));
+ Timeline timeline1 =
+ new FakeTimeline(timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
+ Timeline timeline2 =
+ new FakeTimeline(timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
+
+ assertThat(timeline1).isEqualTo(timeline2);
+ assertThat(timeline1.hashCode()).isEqualTo(timeline2.hashCode());
+ }
+
+ @Test
+ public void timelineEquals_includesShuffleOrder() {
+ ImmutableList timelineWindowDefinitions =
+ ImmutableList.of(
+ new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 111),
+ new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 222),
+ new TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 333));
+ Timeline timeline =
+ new FakeTimeline(
+ new Object[0],
+ new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 5),
+ timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
+ Timeline timelineWithEquivalentShuffleOrder =
+ new FakeTimeline(
+ new Object[0],
+ new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 5),
+ timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
+ Timeline timelineWithDifferentShuffleOrder =
+ new FakeTimeline(
+ new Object[0],
+ new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 3),
+ timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
+
+ assertThat(timeline).isEqualTo(timelineWithEquivalentShuffleOrder);
+ assertThat(timeline.hashCode()).isEqualTo(timelineWithEquivalentShuffleOrder.hashCode());
+ assertThat(timeline).isNotEqualTo(timelineWithDifferentShuffleOrder);
+ }
+
@Test
public void windowEquals() {
MediaItem mediaItem = new MediaItem.Builder().setUri("uri").setTag(new Object()).build();
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
index 389112484a0..76731b4e1c4 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
@@ -294,6 +294,7 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
+ COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS,
COMMAND_GET_AUDIO_ATTRIBUTES,
@@ -303,8 +304,7 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_SET_VIDEO_SURFACE,
- COMMAND_GET_TEXT,
- COMMAND_SET_MEDIA_ITEM)
+ COMMAND_GET_TEXT)
.addIf(
COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported())
.build();
@@ -433,6 +433,9 @@ public DeviceComponent getDeviceComponent() {
public void experimentalSetOffloadSchedulingEnabled(boolean offloadSchedulingEnabled) {
verifyApplicationThread();
internalPlayer.experimentalSetOffloadSchedulingEnabled(offloadSchedulingEnabled);
+ for (AudioOffloadListener listener : audioOffloadListeners) {
+ listener.onExperimentalOffloadSchedulingEnabledChanged(offloadSchedulingEnabled);
+ }
}
@Override
@@ -707,6 +710,7 @@ public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) {
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
verifyApplicationThread();
+ this.shuffleOrder = shuffleOrder;
Timeline timeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
@@ -715,7 +719,6 @@ public void setShuffleOrder(ShuffleOrder shuffleOrder) {
maskWindowPositionMsOrGetPeriodPositionUs(
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
pendingOperationAcks++;
- this.shuffleOrder = shuffleOrder;
internalPlayer.setShuffleOrder(shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
@@ -1962,12 +1965,6 @@ private void updatePlaybackInfo(
updateAvailableCommands();
listeners.flushEvents();
- if (previousPlaybackInfo.offloadSchedulingEnabled != newPlaybackInfo.offloadSchedulingEnabled) {
- for (AudioOffloadListener listener : audioOffloadListeners) {
- listener.onExperimentalOffloadSchedulingEnabledChanged(
- newPlaybackInfo.offloadSchedulingEnabled);
- }
- }
if (previousPlaybackInfo.sleepingForOffload != newPlaybackInfo.sleepingForOffload) {
for (AudioOffloadListener listener : audioOffloadListeners) {
listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java
index 6eba41d01ef..a9f0af61117 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java
@@ -817,10 +817,8 @@ private void setOffloadSchedulingEnabledInternal(boolean offloadSchedulingEnable
return;
}
this.offloadSchedulingEnabled = offloadSchedulingEnabled;
- @Player.State int state = playbackInfo.playbackState;
- if (offloadSchedulingEnabled || state == Player.STATE_ENDED || state == Player.STATE_IDLE) {
- playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled);
- } else {
+ if (!offloadSchedulingEnabled && playbackInfo.sleepingForOffload) {
+ // We need to wake the player up if offload scheduling is disabled and we are sleeping.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
@@ -960,12 +958,14 @@ private void notifyTrackSelectionRebuffer() {
private void doSomeWork() throws ExoPlaybackException, IOException {
long operationStartTimeMs = clock.uptimeMillis();
+ // Remove other pending DO_SOME_WORK requests that are handled by this invocation.
+ handler.removeMessages(MSG_DO_SOME_WORK);
+
updatePeriods();
if (playbackInfo.playbackState == Player.STATE_IDLE
|| playbackInfo.playbackState == Player.STATE_ENDED) {
- // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
- handler.removeMessages(MSG_DO_SOME_WORK);
+ // Nothing to do. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
return;
}
@@ -1078,24 +1078,24 @@ && isLoadingPossible()) {
throw new IllegalStateException("Playback stuck buffering and not loading");
}
- if (offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled) {
- playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled);
- }
-
- boolean sleepingForOffload = false;
- if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
- || playbackInfo.playbackState == Player.STATE_BUFFERING) {
- sleepingForOffload = !maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS);
- } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
- scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
- } else {
- handler.removeMessages(MSG_DO_SOME_WORK);
- }
+ boolean isPlaying = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY;
+ boolean sleepingForOffload = offloadSchedulingEnabled && requestForRendererSleep && isPlaying;
if (playbackInfo.sleepingForOffload != sleepingForOffload) {
playbackInfo = playbackInfo.copyWithSleepingForOffload(sleepingForOffload);
}
requestForRendererSleep = false; // A sleep request is only valid for the current doSomeWork.
+ if (sleepingForOffload || playbackInfo.playbackState == Player.STATE_ENDED) {
+ // No need to schedule next work.
+ return;
+ } else if (isPlaying || playbackInfo.playbackState == Player.STATE_BUFFERING) {
+ // We are actively playing or waiting for data to be ready. Schedule next work quickly.
+ scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
+ } else if (playbackInfo.playbackState == Player.STATE_READY && enabledRendererCount != 0) {
+ // We are ready, but not playing. Schedule next work less often to handle non-urgent updates.
+ scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
+ }
+
TraceUtil.endSection();
}
@@ -1125,19 +1125,9 @@ private boolean shouldUseLivePlaybackSpeedControl(
}
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
- handler.removeMessages(MSG_DO_SOME_WORK);
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
}
- private boolean maybeScheduleWakeup(long operationStartTimeMs, long intervalMs) {
- if (offloadSchedulingEnabled && requestForRendererSleep) {
- return false;
- }
-
- scheduleNextWork(operationStartTimeMs, intervalMs);
- return true;
- }
-
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
@@ -1468,7 +1458,6 @@ private void resetInternal(
/* bufferedPositionUs= */ startPositionUs,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ startPositionUs,
- offloadSchedulingEnabled,
/* sleepingForOffload= */ false);
if (releaseMediaSourceList) {
mediaSourceList.release();
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java
index f0e104a75ac..9ea9b0e971f 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java
@@ -74,8 +74,6 @@
public final @PlaybackSuppressionReason int playbackSuppressionReason;
/** The playback parameters. */
public final PlaybackParameters playbackParameters;
- /** Whether offload scheduling is enabled for the main player loop. */
- public final boolean offloadSchedulingEnabled;
/** Whether the main player loop is sleeping, while using offload scheduling. */
public final boolean sleepingForOffload;
@@ -122,7 +120,6 @@ public static PlaybackInfo createDummy(TrackSelectorResult emptyTrackSelectorRes
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
- /* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
}
@@ -145,7 +142,6 @@ public static PlaybackInfo createDummy(TrackSelectorResult emptyTrackSelectorRes
* @param bufferedPositionUs See {@link #bufferedPositionUs}.
* @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}.
* @param positionUs See {@link #positionUs}.
- * @param offloadSchedulingEnabled See {@link #offloadSchedulingEnabled}.
* @param sleepingForOffload See {@link #sleepingForOffload}.
*/
public PlaybackInfo(
@@ -166,7 +162,6 @@ public PlaybackInfo(
long bufferedPositionUs,
long totalBufferedDurationUs,
long positionUs,
- boolean offloadSchedulingEnabled,
boolean sleepingForOffload) {
this.timeline = timeline;
this.periodId = periodId;
@@ -185,7 +180,6 @@ public PlaybackInfo(
this.bufferedPositionUs = bufferedPositionUs;
this.totalBufferedDurationUs = totalBufferedDurationUs;
this.positionUs = positionUs;
- this.offloadSchedulingEnabled = offloadSchedulingEnabled;
this.sleepingForOffload = sleepingForOffload;
}
@@ -237,7 +231,6 @@ public PlaybackInfo copyWithNewPosition(
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -267,7 +260,6 @@ public PlaybackInfo copyWithTimeline(Timeline timeline) {
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -297,7 +289,6 @@ public PlaybackInfo copyWithPlaybackState(int playbackState) {
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -327,7 +318,6 @@ public PlaybackInfo copyWithPlaybackError(@Nullable ExoPlaybackException playbac
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -357,7 +347,6 @@ public PlaybackInfo copyWithIsLoading(boolean isLoading) {
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -387,7 +376,6 @@ public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPerio
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -421,7 +409,6 @@ public PlaybackInfo copyWithPlayWhenReady(
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -451,38 +438,6 @@ public PlaybackInfo copyWithPlaybackParameters(PlaybackParameters playbackParame
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
- sleepingForOffload);
- }
-
- /**
- * Copies playback info with new offloadSchedulingEnabled.
- *
- * @param offloadSchedulingEnabled New offloadSchedulingEnabled state. See {@link
- * #offloadSchedulingEnabled}.
- * @return Copied playback info with new offload scheduling state.
- */
- @CheckResult
- public PlaybackInfo copyWithOffloadSchedulingEnabled(boolean offloadSchedulingEnabled) {
- return new PlaybackInfo(
- timeline,
- periodId,
- requestedContentPositionUs,
- discontinuityStartPositionUs,
- playbackState,
- playbackError,
- isLoading,
- trackGroups,
- trackSelectorResult,
- staticMetadata,
- loadingMediaPeriodId,
- playWhenReady,
- playbackSuppressionReason,
- playbackParameters,
- bufferedPositionUs,
- totalBufferedDurationUs,
- positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
@@ -512,7 +467,6 @@ public PlaybackInfo copyWithSleepingForOffload(boolean sleepingForOffload) {
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
- offloadSchedulingEnabled,
sleepingForOffload);
}
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java
index c1a34adb680..b53d79c47ea 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java
@@ -29,7 +29,6 @@
import android.media.AudioTrack;
import android.media.PlaybackParams;
import android.media.metrics.LogSessionId;
-import android.os.ConditionVariable;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Pair;
@@ -44,6 +43,8 @@
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.util.Assertions;
+import androidx.media3.common.util.Clock;
+import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@@ -615,7 +616,8 @@ private DefaultAudioSink(Builder builder) {
enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams;
offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED;
audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider;
- releasingConditionVariable = new ConditionVariable(true);
+ releasingConditionVariable = new ConditionVariable(Clock.DEFAULT);
+ releasingConditionVariable.open();
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
@@ -840,13 +842,15 @@ private void flushAudioProcessors() {
}
}
- private void initializeAudioTrack() throws InitializationException {
- // If we're asynchronously releasing a previous audio track then we block until it has been
+ private boolean initializeAudioTrack() throws InitializationException {
+ // If we're asynchronously releasing a previous audio track then we wait until it has been
// released. This guarantees that we cannot end up in a state where we have multiple audio
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
// the shared memory that's available for audio track buffers. This would in turn cause the
// initialization of the audio track to fail.
- releasingConditionVariable.block();
+ if (!releasingConditionVariable.isOpen()) {
+ return false;
+ }
audioTrack = buildAudioTrackWithRetry();
if (isOffloadedPlayback(audioTrack)) {
@@ -874,6 +878,7 @@ private void initializeAudioTrack() throws InitializationException {
}
startMediaTimeUsNeedsInit = true;
+ return true;
}
@Override
@@ -930,7 +935,10 @@ public boolean handleBuffer(
if (!isAudioTrackInitialized()) {
try {
- initializeAudioTrack();
+ if (!initializeAudioTrack()) {
+ // Not yet ready for initialization of a new AudioTrack.
+ return false;
+ }
} catch (InitializationException e) {
if (e.isRecoverable) {
throw e; // Do not delay the exception if it can be recovered at higher level.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java
index a435e6c9efb..745cdc5474c 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java
@@ -317,7 +317,9 @@ private boolean isCodecProfileAndLevelSupported(Format format) {
}
for (CodecProfileLevel profileLevel : profileLevels) {
- if (profileLevel.profile == profile && profileLevel.level >= level) {
+ if (profileLevel.profile == profile
+ && profileLevel.level >= level
+ && !needsProfileExcludedWorkaround(mimeType, profile)) {
return true;
}
}
@@ -831,4 +833,15 @@ private static final boolean needsRotatedVerticalResolutionWorkaround(String nam
}
return true;
}
+
+ /**
+ * Whether a profile is excluded from the list of supported profiles. This may happen when a
+ * device declares support for a profile it doesn't actually support.
+ */
+ private static boolean needsProfileExcludedWorkaround(String mimeType, int profile) {
+ // See https://github.com/google/ExoPlayer/issues/3537
+ return MimeTypes.VIDEO_H265.equals(mimeType)
+ && CodecProfileLevel.HEVCProfileMain10 == profile
+ && ("sailfish".equals(Util.DEVICE) || "marlin".equals(Util.DEVICE));
+ }
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java
index f0a8cb11648..6a55a3a13e7 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java
@@ -282,6 +282,7 @@ public DefaultMediaSourceFactory clearLocalAdInsertionComponents() {
*/
public DefaultMediaSourceFactory setDataSourceFactory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
+ delegateFactoryLoader.setDataSourceFactory(dataSourceFactory);
return this;
}
@@ -594,6 +595,7 @@ public void setDataSourceFactory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
// TODO(b/233577470): Call MediaSource.Factory.setDataSourceFactory on each value when it
// exists on the interface.
+ mediaSourceFactorySuppliers.clear();
mediaSourceFactories.clear();
}
}
@@ -627,6 +629,7 @@ private Supplier maybeLoadSupplier(@C.ContentType int conte
}
@Nullable Supplier mediaSourceFactorySupplier = null;
+ DataSource.Factory dataSourceFactory = checkNotNull(this.dataSourceFactory);
try {
Class extends MediaSource.Factory> clazz;
switch (contentType) {
@@ -634,19 +637,19 @@ private Supplier maybeLoadSupplier(@C.ContentType int conte
clazz =
Class.forName("androidx.media3.exoplayer.dash.DashMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
- mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
+ mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.CONTENT_TYPE_SS:
clazz =
Class.forName("androidx.media3.exoplayer.smoothstreaming.SsMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
- mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
+ mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.CONTENT_TYPE_HLS:
clazz =
Class.forName("androidx.media3.exoplayer.hls.HlsMediaSource$Factory")
.asSubclass(MediaSource.Factory.class);
- mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory));
+ mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory);
break;
case C.CONTENT_TYPE_RTSP:
clazz =
@@ -656,9 +659,7 @@ private Supplier maybeLoadSupplier(@C.ContentType int conte
break;
case C.CONTENT_TYPE_OTHER:
mediaSourceFactorySupplier =
- () ->
- new ProgressiveMediaSource.Factory(
- checkNotNull(dataSourceFactory), extractorsFactory);
+ () -> new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
break;
default:
// Do nothing.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java
index 026917b9a04..7471f3a2bdb 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java
@@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.source;
+import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -135,7 +136,7 @@ interface Listener {
private boolean seenFirstTrackSelection;
private boolean notifyDiscontinuity;
private int enabledTrackCount;
- private long length;
+ private boolean isLengthKnown;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
@@ -193,15 +194,13 @@ public ProgressiveMediaPeriod(
onContinueLoadingRequestedRunnable =
() -> {
if (!released) {
- Assertions.checkNotNull(callback)
- .onContinueLoadingRequested(ProgressiveMediaPeriod.this);
+ checkNotNull(callback).onContinueLoadingRequested(ProgressiveMediaPeriod.this);
}
};
handler = Util.createHandlerForCurrentLooper();
sampleQueueTrackIds = new TrackId[0];
sampleQueues = new SampleQueue[0];
pendingResetPositionUs = C.TIME_UNSET;
- length = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
dataType = C.DATA_TYPE_MEDIA;
}
@@ -367,7 +366,7 @@ public boolean isLoading() {
@Override
public long getNextLoadPositionUs() {
- return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();
+ return getBufferedPositionUs();
}
@Override
@@ -383,8 +382,7 @@ public long readDiscontinuity() {
@Override
public long getBufferedPositionUs() {
assertPrepared();
- boolean[] trackIsAudioVideoFlags = trackState.trackIsAudioVideoFlags;
- if (loadingFinished) {
+ if (loadingFinished || enabledTrackCount == 0) {
return C.TIME_END_OF_SOURCE;
} else if (isPendingReset()) {
return pendingResetPositionUs;
@@ -394,14 +392,16 @@ public long getBufferedPositionUs() {
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
- if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
+ if (trackState.trackIsAudioVideoFlags[i]
+ && trackState.trackEnabledStates[i]
+ && !sampleQueues[i].isLastSampleQueued()) {
largestQueuedTimestampUs =
min(largestQueuedTimestampUs, sampleQueues[i].getLargestQueuedTimestampUs());
}
}
}
if (largestQueuedTimestampUs == Long.MAX_VALUE) {
- largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+ largestQueuedTimestampUs = getLargestQueuedTimestampUs(/* includeDisabledTracks= */ false);
}
return largestQueuedTimestampUs == Long.MIN_VALUE
? lastSeekPositionUs
@@ -537,7 +537,7 @@ private void maybeStartDeferredRetry(int track) {
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
- Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ checkNotNull(callback).onContinueLoadingRequested(this);
}
private boolean suppressRead() {
@@ -551,7 +551,8 @@ public void onLoadCompleted(
ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
if (durationUs == C.TIME_UNSET && seekMap != null) {
boolean isSeekable = seekMap.isSeekable();
- long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+ long largestQueuedTimestampUs =
+ getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true);
durationUs =
largestQueuedTimestampUs == Long.MIN_VALUE
? 0
@@ -578,9 +579,8 @@ public void onLoadCompleted(
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs);
- copyLengthFromLoader(loadable);
loadingFinished = true;
- Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ checkNotNull(callback).onContinueLoadingRequested(this);
}
@Override
@@ -607,12 +607,11 @@ public void onLoadCanceled(
/* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs);
if (!released) {
- copyLengthFromLoader(loadable);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
if (enabledTrackCount > 0) {
- Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
+ checkNotNull(callback).onContinueLoadingRequested(this);
}
}
}
@@ -624,7 +623,6 @@ public LoadErrorAction onLoadError(
long loadDurationMs,
IOException error,
int errorCount) {
- copyLengthFromLoader(loadable);
StatsDataSource dataSource = loadable.dataSource;
LoadEventInfo loadEventInfo =
new LoadEventInfo(
@@ -710,6 +708,10 @@ public void onUpstreamFormatChanged(Format format) {
// Internal methods.
+ private void onLengthKnown() {
+ handler.post(() -> isLengthKnown = true);
+ }
+
private TrackOutput prepareTrackOutput(TrackId id) {
int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
@@ -733,7 +735,7 @@ private TrackOutput prepareTrackOutput(TrackId id) {
private void setSeekMap(SeekMap seekMap) {
this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET);
durationUs = seekMap.getDurationUs();
- isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET;
+ isLive = !isLengthKnown && seekMap.getDurationUs() == C.TIME_UNSET;
dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA;
listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive);
if (!prepared) {
@@ -755,7 +757,7 @@ private void maybeFinishPrepare() {
TrackGroup[] trackArray = new TrackGroup[trackCount];
boolean[] trackIsAudioVideoFlags = new boolean[trackCount];
for (int i = 0; i < trackCount; i++) {
- Format trackFormat = Assertions.checkNotNull(sampleQueues[i].getUpstreamFormat());
+ Format trackFormat = checkNotNull(sampleQueues[i].getUpstreamFormat());
@Nullable String mimeType = trackFormat.sampleMimeType;
boolean isAudio = MimeTypes.isAudio(mimeType);
boolean isAudioVideo = isAudio || MimeTypes.isVideo(mimeType);
@@ -786,13 +788,7 @@ private void maybeFinishPrepare() {
}
trackState = new TrackState(new TrackGroupArray(trackArray), trackIsAudioVideoFlags);
prepared = true;
- Assertions.checkNotNull(callback).onPrepared(this);
- }
-
- private void copyLengthFromLoader(ExtractingLoadable loadable) {
- if (length == C.LENGTH_UNSET) {
- length = loadable.length;
- }
+ checkNotNull(callback).onPrepared(this);
}
private void startLoading() {
@@ -807,7 +803,7 @@ private void startLoading() {
return;
}
loadable.setLoadPosition(
- Assertions.checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position,
+ checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position,
pendingResetPositionUs);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.setStartTimeUs(pendingResetPositionUs);
@@ -840,7 +836,7 @@ private void startLoading() {
* retry.
*/
private boolean configureRetry(ExtractingLoadable loadable, int currentExtractedSampleCount) {
- if (length != C.LENGTH_UNSET || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
+ if (isLengthKnown || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
// We're playing an on-demand stream. Resume the current loadable, which will
// request data starting from the point it left off.
extractedSamplesCountAtStartOfLoad = currentExtractedSampleCount;
@@ -904,11 +900,13 @@ private int getExtractedSamplesCount() {
return extractedSamplesCount;
}
- private long getLargestQueuedTimestampUs() {
+ private long getLargestQueuedTimestampUs(boolean includeDisabledTracks) {
long largestQueuedTimestampUs = Long.MIN_VALUE;
- for (SampleQueue sampleQueue : sampleQueues) {
- largestQueuedTimestampUs =
- max(largestQueuedTimestampUs, sampleQueue.getLargestQueuedTimestampUs());
+ for (int i = 0; i < sampleQueues.length; i++) {
+ if (includeDisabledTracks || checkNotNull(trackState).trackEnabledStates[i]) {
+ largestQueuedTimestampUs =
+ max(largestQueuedTimestampUs, sampleQueues[i].getLargestQueuedTimestampUs());
+ }
}
return largestQueuedTimestampUs;
}
@@ -920,8 +918,8 @@ private boolean isPendingReset() {
@EnsuresNonNull({"trackState", "seekMap"})
private void assertPrepared() {
Assertions.checkState(prepared);
- Assertions.checkNotNull(trackState);
- Assertions.checkNotNull(seekMap);
+ checkNotNull(trackState);
+ checkNotNull(seekMap);
}
private final class SampleStreamImpl implements SampleStream {
@@ -970,7 +968,6 @@ public int skipData(long positionUs) {
private boolean pendingExtractorSeek;
private long seekTimeUs;
private DataSpec dataSpec;
- private long length;
@Nullable private TrackOutput icyTrackOutput;
private boolean seenIcyMetadata;
@@ -988,7 +985,6 @@ public ExtractingLoadable(
this.loadCondition = loadCondition;
this.positionHolder = new PositionHolder();
this.pendingExtractorSeek = true;
- this.length = C.LENGTH_UNSET;
loadTaskId = LoadEventInfo.getNewId();
dataSpec = buildDataSpec(/* position= */ 0);
}
@@ -1007,9 +1003,10 @@ public void load() throws IOException {
try {
long position = positionHolder.position;
dataSpec = buildDataSpec(position);
- length = dataSource.open(dataSpec);
+ long length = dataSource.open(dataSpec);
if (length != C.LENGTH_UNSET) {
length += position;
+ onLengthKnown();
}
icyHeaders = IcyHeaders.parse(dataSource.getResponseHeaders());
DataSource extractorDataSource = dataSource;
@@ -1065,9 +1062,12 @@ public void load() throws IOException {
public void onIcyMetadata(ParsableByteArray metadata) {
// Always output the first ICY metadata at the start time. This helps minimize any delay
// between the start of playback and the first ICY metadata event.
- long timeUs = !seenIcyMetadata ? seekTimeUs : max(getLargestQueuedTimestampUs(), seekTimeUs);
+ long timeUs =
+ !seenIcyMetadata
+ ? seekTimeUs
+ : max(getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true), seekTimeUs);
int length = metadata.bytesLeft();
- TrackOutput icyTrackOutput = Assertions.checkNotNull(this.icyTrackOutput);
+ TrackOutput icyTrackOutput = checkNotNull(this.icyTrackOutput);
icyTrackOutput.sampleData(metadata, length);
icyTrackOutput.sampleMetadata(
timeUs, C.BUFFER_FLAG_KEY_FRAME, length, /* offset= */ 0, /* cryptoData= */ null);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextOutput.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextOutput.java
index 5485452d965..6e12cd57fbc 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextOutput.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/TextOutput.java
@@ -39,7 +39,7 @@ default void onCues(List cues) {}
* Called when there is a change in the {@link CueGroup}.
*
* Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
- * in the cues You should only implement one or the other.
+ * in the cues. You should only implement one or the other.
*/
void onCues(CueGroup cueGroup);
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
index 7773fa039e4..6c091844a4a 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
@@ -629,7 +629,7 @@ private void setOutput(@Nullable Object output) throws ExoPlaybackException {
surface = placeholderSurface;
} else {
MediaCodecInfo codecInfo = getCodecInfo();
- if (codecInfo != null && shouldUseDummySurface(codecInfo)) {
+ if (codecInfo != null && shouldUsePlaceholderSurface(codecInfo)) {
placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure);
surface = placeholderSurface;
}
@@ -675,7 +675,7 @@ private void setOutput(@Nullable Object output) throws ExoPlaybackException {
@Override
protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
- return surface != null || shouldUseDummySurface(codecInfo);
+ return surface != null || shouldUsePlaceholderSurface(codecInfo);
}
@Override
@@ -706,7 +706,7 @@ protected MediaCodecAdapter.Configuration getMediaCodecConfiguration(
deviceNeedsNoPostProcessWorkaround,
tunneling ? tunnelingAudioSessionId : C.AUDIO_SESSION_ID_UNSET);
if (surface == null) {
- if (!shouldUseDummySurface(codecInfo)) {
+ if (!shouldUsePlaceholderSurface(codecInfo)) {
throw new IllegalStateException();
}
if (placeholderSurface == null) {
@@ -1333,7 +1333,7 @@ protected void renderOutputBufferV21(
maybeNotifyRenderedFirstFrame();
}
- private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) {
+ private boolean shouldUsePlaceholderSurface(MediaCodecInfo codecInfo) {
return Util.SDK_INT >= 23
&& !tunneling
&& !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name)
@@ -1572,7 +1572,7 @@ protected CodecMaxValues getCodecMaxValues(
}
if (haveUnknownDimensions) {
Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight);
- Point codecMaxSize = getCodecMaxSize(codecInfo, format);
+ @Nullable Point codecMaxSize = getCodecMaxSize(codecInfo, format);
if (codecMaxSize != null) {
maxWidth = max(maxWidth, codecMaxSize.x);
maxHeight = max(maxHeight, codecMaxSize.y);
@@ -1600,8 +1600,10 @@ protected MediaCodecDecoderException createDecoderException(
*
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The {@link Format} for which the codec is being configured.
- * @return The maximum video size to use, or null if the size of {@code format} should be used.
+ * @return The maximum video size to use, or {@code null} if the size of {@code format} should be
+ * used.
*/
+ @Nullable
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {
boolean isVerticalVideo = format.height > format.width;
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
index 784f6c23df3..7c1382bba9d 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
@@ -53,13 +53,13 @@
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static androidx.media3.test.utils.TestUtil.assertTimelinesSame;
+import static androidx.media3.test.utils.TestUtil.timelinesAreSame;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
-import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged;
import static com.google.common.truth.Truth.assertThat;
@@ -125,6 +125,7 @@
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
+import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
@@ -157,7 +158,6 @@
import androidx.media3.test.utils.FakeTrackSelection;
import androidx.media3.test.utils.FakeTrackSelector;
import androidx.media3.test.utils.FakeVideoRenderer;
-import androidx.media3.test.utils.NoUidTimeline;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
@@ -6512,6 +6512,53 @@ public void run(ExoPlayer player) {
assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000);
}
+ @Test
+ public void setShuffleOrder_notifiesTimelineChanged() throws Exception {
+ ExoPlayer player =
+ new TestExoPlayerBuilder(context)
+ .setClock(new FakeClock(/* isAutoAdvancing= */ true))
+ .build();
+ // No callback expected for this call, because the (empty) timeline doesn't change. We start
+ // with a deterministic shuffle order, to ensure when we call setShuffleOrder again below the
+ // order is definitely different (otherwise the test is flaky when the existing shuffle order
+ // matches the shuffle order passed in below).
+ player.setShuffleOrder(new FakeShuffleOrder(0));
+ player.setMediaSources(
+ ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
+ Player.Listener mockListener = mock(Player.Listener.class);
+ player.addListener(mockListener);
+ player.prepare();
+ TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 5000);
+ player.play();
+ ShuffleOrder.DefaultShuffleOrder newShuffleOrder =
+ new ShuffleOrder.DefaultShuffleOrder(player.getMediaItemCount(), /* randomSeed= */ 5);
+ player.setShuffleOrder(newShuffleOrder);
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
+ player.release();
+
+ ArgumentCaptor timelineCaptor = ArgumentCaptor.forClass(Timeline.class);
+ verify(mockListener)
+ .onTimelineChanged(
+ timelineCaptor.capture(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
+
+ Timeline capturedTimeline = Iterables.getOnlyElement(timelineCaptor.getAllValues());
+ List newShuffleOrderIndexes = new ArrayList<>(newShuffleOrder.getLength());
+ for (int i = newShuffleOrder.getFirstIndex();
+ i != C.INDEX_UNSET;
+ i = newShuffleOrder.getNextIndex(i)) {
+ newShuffleOrderIndexes.add(i);
+ }
+ List capturedTimelineShuffleIndexes = new ArrayList<>(newShuffleOrder.getLength());
+ for (int i = capturedTimeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true);
+ i != C.INDEX_UNSET;
+ i =
+ capturedTimeline.getNextWindowIndex(
+ i, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) {
+ capturedTimelineShuffleIndexes.add(i);
+ }
+ assertThat(capturedTimelineShuffleIndexes).isEqualTo(newShuffleOrderIndexes);
+ }
+
@Test
public void setMediaSources_empty_whenEmpty_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
@@ -9635,47 +9682,16 @@ protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
}
@Test
- public void enableOffloadSchedulingWhileIdle_isToggled_isReported() throws Exception {
+ public void enableOffloadScheduling_isReported() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
+ ExoPlayer.AudioOffloadListener mockListener = mock(ExoPlayer.AudioOffloadListener.class);
+ player.addAudioOffloadListener(mockListener);
player.experimentalSetOffloadSchedulingEnabled(true);
- assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue();
+ verify(mockListener).onExperimentalOffloadSchedulingEnabledChanged(true);
player.experimentalSetOffloadSchedulingEnabled(false);
- assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
- }
-
- @Test
- public void enableOffloadSchedulingWhilePlaying_isToggled_isReported() throws Exception {
- FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
- ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
- Timeline timeline = new FakeTimeline();
- player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
- player.prepare();
- player.play();
-
- player.experimentalSetOffloadSchedulingEnabled(true);
- assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue();
-
- player.experimentalSetOffloadSchedulingEnabled(false);
- assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
- }
-
- @Test
- public void enableOffloadSchedulingWhileSleepingForOffload_isDisabled_isReported()
- throws Exception {
- FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
- ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
- Timeline timeline = new FakeTimeline();
- player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
- player.experimentalSetOffloadSchedulingEnabled(true);
- player.prepare();
- player.play();
- runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
-
- player.experimentalSetOffloadSchedulingEnabled(false);
-
- assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
+ verify(mockListener).onExperimentalOffloadSchedulingEnabledChanged(false);
}
@Test
@@ -12296,6 +12312,6 @@ public Loader.LoadErrorAction onLoadError(
* Returns an argument matcher for {@link Timeline} instances that ignores period and window uids.
*/
private static ArgumentMatcher noUid(Timeline timeline) {
- return argument -> new NoUidTimeline(timeline).equals(new NoUidTimeline(argument));
+ return argument -> timelinesAreSame(argument, timeline);
}
}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java
index 155450ec7e1..cb059bc241c 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java
@@ -1112,7 +1112,6 @@ private void setupTimeline(Timeline timeline) {
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
- /* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java
index be3ccd914bf..b7ad234f18f 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java
@@ -57,6 +57,7 @@ public static ImmutableList mediaSamples() {
"sample_eac3joc.mp4",
"sample_fragmented.mp4",
"sample_fragmented_seekable.mp4",
+ "sample_fragmented_large_bitrates.mp4",
"sample_fragmented_sei.mp4",
"sample_mdat_too_long.mp4",
"sample.mp4",
diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java
index 36c3695193d..6c4e9bee10b 100644
--- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java
+++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java
@@ -599,6 +599,9 @@ protected AdaptationSet buildAdaptationSet(
case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
uuid = C.WIDEVINE_UUID;
break;
+ case "urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e":
+ uuid = C.CLEARKEY_UUID;
+ break;
default:
break;
}
@@ -606,7 +609,9 @@ protected AdaptationSet buildAdaptationSet(
do {
xpp.next();
- if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
+ if (XmlPullParserUtil.isStartTag(xpp, "clearkey:Laurl") && xpp.next() == XmlPullParser.TEXT) {
+ licenseServerUrl = xpp.getText();
+ } else if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl");
} else if (data == null
&& XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh")
@@ -853,6 +858,7 @@ protected Representation buildRepresentation(
ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
+ fillInClearKeyInformation(drmSchemeDatas);
filterRedundantIncompleteSchemeDatas(drmSchemeDatas);
formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas));
}
@@ -1660,6 +1666,32 @@ private static void filterRedundantIncompleteSchemeDatas(ArrayList s
}
}
+ private static void fillInClearKeyInformation(ArrayList schemeDatas) {
+ // Find and remove ClearKey information.
+ @Nullable String clearKeyLicenseServerUrl = null;
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ if (C.CLEARKEY_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl != null) {
+ clearKeyLicenseServerUrl = schemeData.licenseServerUrl;
+ schemeDatas.remove(i);
+ break;
+ }
+ }
+ if (clearKeyLicenseServerUrl == null) {
+ return;
+ }
+ // Fill in the ClearKey information into the existing PSSH schema data if applicable.
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ if (C.COMMON_PSSH_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl == null) {
+ schemeDatas.set(
+ i,
+ new SchemeData(
+ C.CLEARKEY_UUID, clearKeyLicenseServerUrl, schemeData.mimeType, schemeData.data));
+ }
+ }
+ }
+
/**
* Derives a sample mimeType from a container mimeType and codecs attribute.
*
diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java
index b6d4ac102f5..77c1dde3112 100644
--- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java
+++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java
@@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.dash;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
+import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.test.utils.FakeDataSource;
+import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -82,4 +87,53 @@ public void getSupportedTypes_dashModule_containsTypeDash() {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_DASH);
}
+
+ @Test
+ public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
+ .setDataSourceFactory(() -> fakeDataSource);
+
+ prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ @Test
+ public void
+ createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
+ throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
+
+ // Use default DataSource.Factory first.
+ prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+ defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
+ prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ private static void prepareDashUrlAndWaitForPrepareError(
+ DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
+ MediaSource mediaSource =
+ defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.mpd"));
+ getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mediaSource.prepareSource(
+ (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
+ // We don't expect this to prepare successfully.
+ RobolectricUtil.runMainLooperUntil(
+ () -> {
+ try {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return false;
+ } catch (IOException e) {
+ return true;
+ }
+ });
+ }
}
diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java
index 667f55c197e..76193227696 100644
--- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java
+++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java
@@ -20,6 +20,7 @@
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
+import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
@@ -79,6 +80,8 @@ public class DashManifestParserTest {
"media/mpd/sample_mpd_service_description_low_latency_only_playback_rates";
private static final String SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_ONLY_TARGET_LATENCY =
"media/mpd/sample_mpd_service_description_low_latency_only_target_latency";
+ private static final String SAMPLE_MPD_CLEAR_KEY_LICENSE_URL =
+ "media/mpd/sample_mpd_clear_key_license_url";
private static final String NEXT_TAG_NAME = "Next";
private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>";
@@ -880,6 +883,37 @@ public void serviceDescriptionElement_noServiceDescription_isNullInManifest() th
assertThat(manifest.serviceDescription).isNull();
}
+ @Test
+ public void contentProtections_withClearKeyLicenseUrl() throws IOException {
+ DashManifestParser parser = new DashManifestParser();
+
+ DashManifest manifest =
+ parser.parse(
+ Uri.parse("https://example.com/test.mpd"),
+ TestUtil.getInputStream(
+ ApplicationProvider.getApplicationContext(), SAMPLE_MPD_CLEAR_KEY_LICENSE_URL));
+
+ assertThat(manifest.getPeriodCount()).isEqualTo(1);
+ Period period = manifest.getPeriod(0);
+ assertThat(period.adaptationSets).hasSize(2);
+ AdaptationSet adaptationSet0 = period.adaptationSets.get(0);
+ AdaptationSet adaptationSet1 = period.adaptationSets.get(1);
+ assertThat(adaptationSet0.representations).hasSize(1);
+ assertThat(adaptationSet1.representations).hasSize(1);
+ Representation representation0 = adaptationSet0.representations.get(0);
+ Representation representation1 = adaptationSet1.representations.get(0);
+ assertThat(representation0.format.drmInitData.schemeType).isEqualTo("cenc");
+ assertThat(representation1.format.drmInitData.schemeType).isEqualTo("cenc");
+ assertThat(representation0.format.drmInitData.schemeDataCount).isEqualTo(1);
+ assertThat(representation1.format.drmInitData.schemeDataCount).isEqualTo(1);
+ DrmInitData.SchemeData schemeData0 = representation0.format.drmInitData.get(0);
+ DrmInitData.SchemeData schemeData1 = representation1.format.drmInitData.get(0);
+ assertThat(schemeData0.uuid).isEqualTo(C.CLEARKEY_UUID);
+ assertThat(schemeData1.uuid).isEqualTo(C.CLEARKEY_UUID);
+ assertThat(schemeData0.licenseServerUrl).isEqualTo("https://testserver1.test/AcquireLicense");
+ assertThat(schemeData1.licenseServerUrl).isEqualTo("https://testserver2.test/AcquireLicense");
+ }
+
private static List buildCea608AccessibilityDescriptors(String value) {
return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null));
}
diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java
index 2a2ff66b280..8062cff051a 100644
--- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java
+++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java
@@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.hls;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
+import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.test.utils.FakeDataSource;
+import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -82,4 +87,53 @@ public void getSupportedTypes_hlsModule_containsTypeHls() {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_HLS);
}
+
+ @Test
+ public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
+ .setDataSourceFactory(() -> fakeDataSource);
+
+ prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ @Test
+ public void
+ createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
+ throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
+
+ // Use default DataSource.Factory first.
+ prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+ defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
+ prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ private static void prepareHlsUrlAndWaitForPrepareError(
+ DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
+ MediaSource mediaSource =
+ defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.m3u8"));
+ getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mediaSource.prepareSource(
+ (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
+ // We don't expect this to prepare successfully.
+ RobolectricUtil.runMainLooperUntil(
+ () -> {
+ try {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return false;
+ } catch (IOException e) {
+ return true;
+ }
+ });
+ }
}
diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java
index 72b739edd23..31bc245f8e7 100644
--- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java
+++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java
@@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer.rtsp.reader;
+import static androidx.media3.common.util.Assertions.checkNotNull;
+import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import androidx.media3.common.C;
@@ -51,6 +53,8 @@
/** The combined size of a sample that is fragmented into multiple RTP packets. */
private int fragmentedSampleSizeBytes;
+ private long fragmentedSampleTimeUs;
+
private long startTimeOffsetUs;
/**
* Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP
@@ -67,6 +71,7 @@ public RtpVp8Reader(RtpPayloadFormat payloadFormat) {
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
+ fragmentedSampleTimeUs = C.TIME_UNSET;
// The start time offset must be 0 until the first seek.
startTimeOffsetUs = 0;
gotFirstPacketOfVp8Frame = false;
@@ -81,7 +86,10 @@ public void createTracks(ExtractorOutput extractorOutput, int trackId) {
}
@Override
- public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
+ public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
+ checkState(firstReceivedTimestamp == C.TIME_UNSET);
+ firstReceivedTimestamp = timestamp;
+ }
@Override
public void consume(
@@ -113,21 +121,16 @@ public void consume(
int fragmentSize = data.bytesLeft();
trackOutput.sampleData(data, fragmentSize);
- fragmentedSampleSizeBytes += fragmentSize;
+ if (fragmentedSampleSizeBytes == C.LENGTH_UNSET) {
+ fragmentedSampleSizeBytes = fragmentSize;
+ } else {
+ fragmentedSampleSizeBytes += fragmentSize;
+ }
+
+ fragmentedSampleTimeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
if (rtpMarker) {
- if (firstReceivedTimestamp == C.TIME_UNSET) {
- firstReceivedTimestamp = timestamp;
- }
- long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
- trackOutput.sampleMetadata(
- timeUs,
- isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
- fragmentedSampleSizeBytes,
- /* offset= */ 0,
- /* cryptoData= */ null);
- fragmentedSampleSizeBytes = C.LENGTH_UNSET;
- gotFirstPacketOfVp8Frame = false;
+ outputSampleMetadataForFragmentedPackets();
}
previousSequenceNumber = sequenceNumber;
}
@@ -147,18 +150,18 @@ public void seek(long nextRtpTimestamp, long timeUs) {
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
int header = payload.readUnsignedByte();
- if (!gotFirstPacketOfVp8Frame) {
- // TODO(b/198620566) Consider using ParsableBitArray.
- // For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
- if ((header & 0x10) != 0x1 || (header & 0x07) != 0) {
- Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
- return false;
+ // TODO(b/198620566) Consider using ParsableBitArray.
+ // For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
+ if ((header & 0x10) == 0x10 && (header & 0x07) == 0) {
+ if (gotFirstPacketOfVp8Frame && fragmentedSampleSizeBytes > 0) {
+ // Received new VP8 fragment, output data of previous fragment to decoder.
+ outputSampleMetadataForFragmentedPackets();
}
gotFirstPacketOfVp8Frame = true;
- } else {
+ } else if (gotFirstPacketOfVp8Frame) {
// Check that this packet is in the sequence of the previous packet.
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
- if (packetSequenceNumber != expectedSequenceNumber) {
+ if (packetSequenceNumber < expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
@@ -167,6 +170,9 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
expectedSequenceNumber, packetSequenceNumber));
return false;
}
+ } else {
+ Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
+ return false;
}
// Check if optional X header is present.
@@ -195,6 +201,24 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
return true;
}
+ /**
+ * Outputs sample metadata of the received fragmented packets.
+ *
+ * Call this method only after receiving an end of a VP8 partition.
+ */
+ private void outputSampleMetadataForFragmentedPackets() {
+ checkNotNull(trackOutput)
+ .sampleMetadata(
+ fragmentedSampleTimeUs,
+ isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
+ fragmentedSampleSizeBytes,
+ /* offset= */ 0,
+ /* cryptoData= */ null);
+ fragmentedSampleSizeBytes = 0;
+ fragmentedSampleTimeUs = C.TIME_UNSET;
+ gotFirstPacketOfVp8Frame = false;
+ }
+
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs
diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java
new file mode 100644
index 00000000000..61f80c6c2de
--- /dev/null
+++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2022 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 androidx.media3.exoplayer.rtsp.reader;
+
+import static androidx.media3.common.util.Util.getBytesFromHexString;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.media3.common.C;
+import androidx.media3.common.Format;
+import androidx.media3.common.MimeTypes;
+import androidx.media3.common.util.ParsableByteArray;
+import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.rtsp.RtpPacket;
+import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
+import androidx.media3.test.utils.FakeExtractorOutput;
+import androidx.media3.test.utils.FakeTrackOutput;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Bytes;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link RtpVp8Reader}. */
+@RunWith(AndroidJUnit4.class)
+public final class RtpVp8ReaderTest {
+
+ /** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */
+ private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
+
+ private static final byte[] PARTITION_1 = getBytesFromHexString("000102030405060708090A0B0C0D0E");
+ // 000102030405060708090A
+ private static final byte[] PARTITION_1_FRAGMENT_1 =
+ Arrays.copyOf(PARTITION_1, /* newLength= */ 11);
+ // 0B0C0D0E
+ private static final byte[] PARTITION_1_FRAGMENT_2 =
+ Arrays.copyOfRange(PARTITION_1, /* from= */ 11, /* to= */ 15);
+ private static final long PARTITION_1_RTP_TIMESTAMP = 2599168056L;
+ private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_1 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_1_RTP_TIMESTAMP)
+ .setSequenceNumber(40289)
+ .setMarker(false)
+ .setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_1_FRAGMENT_1))
+ .build();
+ private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_2 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_1_RTP_TIMESTAMP)
+ .setSequenceNumber(40290)
+ .setMarker(false)
+ .setPayloadData(Bytes.concat(getBytesFromHexString("00"), PARTITION_1_FRAGMENT_2))
+ .build();
+
+ private static final byte[] PARTITION_2 = getBytesFromHexString("0D0C0B0A09080706050403020100");
+ // 0D0C0B0A090807060504
+ private static final byte[] PARTITION_2_FRAGMENT_1 =
+ Arrays.copyOf(PARTITION_2, /* newLength= */ 10);
+ // 03020100
+ private static final byte[] PARTITION_2_FRAGMENT_2 =
+ Arrays.copyOfRange(PARTITION_2, /* from= */ 10, /* to= */ 14);
+ private static final long PARTITION_2_RTP_TIMESTAMP = 2599168344L;
+ private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_1 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_2_RTP_TIMESTAMP)
+ .setSequenceNumber(40291)
+ .setMarker(false)
+ .setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_2_FRAGMENT_1))
+ .build();
+ private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_2 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_2_RTP_TIMESTAMP)
+ .setSequenceNumber(40292)
+ .setMarker(true)
+ .setPayloadData(
+ Bytes.concat(
+ getBytesFromHexString("80"),
+ // Optional header.
+ getBytesFromHexString("D6AA953961"),
+ PARTITION_2_FRAGMENT_2))
+ .build();
+ private static final long PARTITION_2_PRESENTATION_TIMESTAMP_US =
+ Util.scaleLargeTimestamp(
+ (PARTITION_2_RTP_TIMESTAMP - PARTITION_1_RTP_TIMESTAMP),
+ /* multiplier= */ C.MICROS_PER_SECOND,
+ /* divisor= */ MEDIA_CLOCK_FREQUENCY);
+
+ private FakeExtractorOutput extractorOutput;
+
+ @Before
+ public void setUp() {
+ extractorOutput =
+ new FakeExtractorOutput(
+ (id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
+ }
+
+ @Test
+ public void consume_validPackets() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(2);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
+ assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ @Test
+ public void consume_fragmentedFrameMissingFirstFragment() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ // First packet timing information is transmitted over RTSP, not RTP.
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(1);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ @Test
+ public void consume_fragmentedFrameMissingBoundaryFragment() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(2);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
+ assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ @Test
+ public void consume_outOfOrderFragmentedFrame() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(2);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
+ assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ private static RtpVp8Reader createVp8Reader() {
+ return new RtpVp8Reader(
+ new RtpPayloadFormat(
+ new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_VP8).build(),
+ /* rtpPayloadType= */ 96,
+ /* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY,
+ /* fmtpParameters= */ ImmutableMap.of()));
+ }
+
+ private static void consume(RtpVp8Reader vp8Reader, RtpPacket rtpPacket) {
+ vp8Reader.consume(
+ new ParsableByteArray(rtpPacket.payloadData),
+ rtpPacket.timestamp,
+ rtpPacket.sequenceNumber,
+ rtpPacket.marker);
+ }
+}
diff --git a/libraries/exoplayer_smoothstreaming/build.gradle b/libraries/exoplayer_smoothstreaming/build.gradle
index a379d25558d..4b145ec6b3c 100644
--- a/libraries/exoplayer_smoothstreaming/build.gradle
+++ b/libraries/exoplayer_smoothstreaming/build.gradle
@@ -29,6 +29,7 @@ dependencies {
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ testImplementation project(modulePrefix + 'test-utils-robolectric')
testImplementation project(modulePrefix + 'test-utils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java
index f5a205fcbe7..4036fb94728 100644
--- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java
+++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java
@@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.smoothstreaming;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
+import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.test.utils.FakeDataSource;
+import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,4 +98,53 @@ public void getSupportedTypes_smoothstreamingModule_containsTypeSS() {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_SS);
}
+
+ @Test
+ public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
+ .setDataSourceFactory(() -> fakeDataSource);
+
+ prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ @Test
+ public void
+ createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
+ throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
+
+ // Use default DataSource.Factory first.
+ prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+ defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
+ prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ private static void prepareSsUrlAndWaitForPrepareError(
+ DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
+ MediaSource mediaSource =
+ defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.ism"));
+ getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mediaSource.prepareSource(
+ (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
+ // We don't expect this to prepare successfully.
+ RobolectricUtil.runMainLooperUntil(
+ () -> {
+ try {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return false;
+ } catch (IOException e) {
+ return true;
+ }
+ });
+ }
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java
index c61d7eaba5b..2a401cbbae1 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java
@@ -18,6 +18,7 @@
import static java.lang.Math.min;
import androidx.annotation.Nullable;
+import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
@@ -786,40 +787,105 @@ private static void skipH265ScalingList(ParsableNalUnitBitArray bitArray) {
}
}
+ /**
+ * Skips any short term reference picture sets contained in a SPS.
+ *
+ *
Note: The st_ref_pic_set parsing in this method is simplified for the case where they're
+ * contained in a SPS, and would need generalizing for use elsewhere.
+ */
private static void skipShortTermReferencePictureSets(ParsableNalUnitBitArray bitArray) {
int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();
- boolean interRefPicSetPredictionFlag = false;
- int numNegativePics;
- int numPositivePics;
- // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous
- // one, so we just keep track of that rather than storing the whole array.
- // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.
- int previousNumDeltaPocs = 0;
+ // As this method applies in a SPS, each short term reference picture set only accesses data
+ // from the previous one. This is because RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1), and
+ // delta_idx_minus1 is always zero in a SPS. Hence we just keep track of variables from the
+ // previous one as we iterate.
+ int previousNumNegativePics = C.INDEX_UNSET;
+ int previousNumPositivePics = C.INDEX_UNSET;
+ int[] previousDeltaPocS0 = new int[0];
+ int[] previousDeltaPocS1 = new int[0];
for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
- if (stRpsIdx != 0) {
- interRefPicSetPredictionFlag = bitArray.readBit();
- }
+ int numNegativePics;
+ int numPositivePics;
+ int[] deltaPocS0;
+ int[] deltaPocS1;
+
+ boolean interRefPicSetPredictionFlag = stRpsIdx != 0 && bitArray.readBit();
if (interRefPicSetPredictionFlag) {
- bitArray.skipBit(); // delta_rps_sign
- bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1
+ int previousNumDeltaPocs = previousNumNegativePics + previousNumPositivePics;
+
+ int deltaRpsSign = bitArray.readBit() ? 1 : 0;
+ int absDeltaRps = bitArray.readUnsignedExpGolombCodedInt() + 1;
+ int deltaRps = (1 - 2 * deltaRpsSign) * absDeltaRps;
+
+ boolean[] useDeltaFlags = new boolean[previousNumDeltaPocs + 1];
for (int j = 0; j <= previousNumDeltaPocs; j++) {
if (!bitArray.readBit()) { // used_by_curr_pic_flag[j]
- bitArray.skipBit(); // use_delta_flag[j]
+ useDeltaFlags[j] = bitArray.readBit();
+ } else {
+ // When use_delta_flag[j] is not present, its value is 1.
+ useDeltaFlags[j] = true;
+ }
+ }
+
+ // Derive numNegativePics, numPositivePics, deltaPocS0 and deltaPocS1 as per Rec. ITU-T
+ // H.265 v6 (06/2019) Section 7.4.8
+ int i = 0;
+ deltaPocS0 = new int[previousNumDeltaPocs + 1];
+ deltaPocS1 = new int[previousNumDeltaPocs + 1];
+ for (int j = previousNumPositivePics - 1; j >= 0; j--) {
+ int dPoc = previousDeltaPocS1[j] + deltaRps;
+ if (dPoc < 0 && useDeltaFlags[previousNumNegativePics + j]) {
+ deltaPocS0[i++] = dPoc;
+ }
+ }
+ if (deltaRps < 0 && useDeltaFlags[previousNumDeltaPocs]) {
+ deltaPocS0[i++] = deltaRps;
+ }
+ for (int j = 0; j < previousNumNegativePics; j++) {
+ int dPoc = previousDeltaPocS0[j] + deltaRps;
+ if (dPoc < 0 && useDeltaFlags[j]) {
+ deltaPocS0[i++] = dPoc;
+ }
+ }
+ numNegativePics = i;
+ deltaPocS0 = Arrays.copyOf(deltaPocS0, numNegativePics);
+
+ i = 0;
+ for (int j = previousNumNegativePics - 1; j >= 0; j--) {
+ int dPoc = previousDeltaPocS0[j] + deltaRps;
+ if (dPoc > 0 && useDeltaFlags[j]) {
+ deltaPocS1[i++] = dPoc;
+ }
+ }
+ if (deltaRps > 0 && useDeltaFlags[previousNumDeltaPocs]) {
+ deltaPocS1[i++] = deltaRps;
+ }
+ for (int j = 0; j < previousNumPositivePics; j++) {
+ int dPoc = previousDeltaPocS1[j] + deltaRps;
+ if (dPoc > 0 && useDeltaFlags[previousNumNegativePics + j]) {
+ deltaPocS1[i++] = dPoc;
}
}
+ numPositivePics = i;
+ deltaPocS1 = Arrays.copyOf(deltaPocS1, numPositivePics);
} else {
numNegativePics = bitArray.readUnsignedExpGolombCodedInt();
numPositivePics = bitArray.readUnsignedExpGolombCodedInt();
- previousNumDeltaPocs = numNegativePics + numPositivePics;
+ deltaPocS0 = new int[numNegativePics];
for (int i = 0; i < numNegativePics; i++) {
- bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i]
+ deltaPocS0[i] = bitArray.readUnsignedExpGolombCodedInt() + 1;
bitArray.skipBit(); // used_by_curr_pic_s0_flag[i]
}
+ deltaPocS1 = new int[numPositivePics];
for (int i = 0; i < numPositivePics; i++) {
- bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]
+ deltaPocS1[i] = bitArray.readUnsignedExpGolombCodedInt() + 1;
bitArray.skipBit(); // used_by_curr_pic_s1_flag[i]
}
}
+ previousNumNegativePics = numNegativePics;
+ previousNumPositivePics = numPositivePics;
+ previousDeltaPocS0 = deltaPocS0;
+ previousDeltaPocS1 = deltaPocS1;
}
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java
index 05511fc7e59..4543d32819b 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java
@@ -45,6 +45,7 @@
import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
@@ -1303,7 +1304,9 @@ private static void parseVideoSampleEntry(
}
if (esdsData != null) {
- formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
+ formatBuilder
+ .setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
+ .setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
}
out.format = formatBuilder.build();
@@ -1609,7 +1612,9 @@ private static void parseAudioSampleEntry(
.setLanguage(language);
if (esdsData != null) {
- formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
+ formatBuilder
+ .setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
+ .setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
}
out.format = formatBuilder.build();
@@ -1659,7 +1664,7 @@ private static EsdsData parseEsdsFromParent(ParsableByteArray parent, int positi
parent.skipBytes(2);
}
if ((flags & 0x40 /* URL_Flag */) != 0) {
- parent.skipBytes(parent.readUnsignedShort());
+ parent.skipBytes(parent.readUnsignedByte());
}
if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
parent.skipBytes(2);
@@ -1683,8 +1688,8 @@ private static EsdsData parseEsdsFromParent(ParsableByteArray parent, int positi
}
parent.skipBytes(4);
- int peakBitrate = parent.readUnsignedIntToInt();
- int bitrate = parent.readUnsignedIntToInt();
+ long peakBitrate = parent.readUnsignedInt();
+ long bitrate = parent.readUnsignedInt();
// Start of the DecoderSpecificInfo.
parent.skipBytes(1); // DecoderSpecificInfo tag
@@ -1943,14 +1948,14 @@ public StsdData(int numberOfEntries) {
private static final class EsdsData {
private final @NullableType String mimeType;
private final byte @NullableType [] initializationData;
- private final int bitrate;
- private final int peakBitrate;
+ private final long bitrate;
+ private final long peakBitrate;
public EsdsData(
@NullableType String mimeType,
byte @NullableType [] initializationData,
- int bitrate,
- int peakBitrate) {
+ long bitrate,
+ long peakBitrate) {
this.mimeType = mimeType;
this.initializationData = initializationData;
this.bitrate = bitrate;
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java
index 01d7fe15f97..59dd8543dbb 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java
@@ -170,6 +170,32 @@ public void discardToSps() {
assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF");
}
+ /** Regression test for https://github.com/google/ExoPlayer/issues/10316. */
+ @Test
+ public void parseH265SpsNalUnitPayload_exoghi_10316() {
+ byte[] spsNalUnitPayload =
+ new byte[] {
+ 1, 2, 32, 0, 0, 3, 0, -112, 0, 0, 3, 0, 0, 3, 0, -106, -96, 1, -32, 32, 2, 28, 77, -98,
+ 87, -110, 66, -111, -123, 22, 74, -86, -53, -101, -98, -68, -28, 9, 119, -21, -103, 120,
+ -16, 22, -95, 34, 1, 54, -62, 0, 0, 7, -46, 0, 0, -69, -127, -12, 85, -17, 126, 0, -29,
+ -128, 28, 120, 1, -57, 0, 56, -15
+ };
+
+ NalUnitUtil.H265SpsData spsData =
+ NalUnitUtil.parseH265SpsNalUnitPayload(spsNalUnitPayload, 0, spsNalUnitPayload.length);
+
+ assertThat(spsData.constraintBytes).isEqualTo(new int[] {144, 0, 0, 0, 0, 0});
+ assertThat(spsData.generalLevelIdc).isEqualTo(150);
+ assertThat(spsData.generalProfileCompatibilityFlags).isEqualTo(4);
+ assertThat(spsData.generalProfileIdc).isEqualTo(2);
+ assertThat(spsData.generalProfileSpace).isEqualTo(0);
+ assertThat(spsData.generalTierFlag).isFalse();
+ assertThat(spsData.height).isEqualTo(2160);
+ assertThat(spsData.pixelWidthHeightRatio).isEqualTo(1);
+ assertThat(spsData.seqParameterSetId).isEqualTo(0);
+ assertThat(spsData.width).isEqualTo(3840);
+ }
+
private static byte[] buildTestData() {
byte[] data = new byte[20];
for (int i = 0; i < data.length; i++) {
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java
index 07c663d426f..269ac4291c2 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java
@@ -122,6 +122,15 @@ public void samplePartiallyFragmented() throws Exception {
simulationConfig);
}
+ /** https://github.com/google/ExoPlayer/issues/10381 */
+ @Test
+ public void sampleWithLargeBitrates() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ getExtractorFactory(ImmutableList.of()),
+ "media/mp4/sample_fragmented_large_bitrates.mp4",
+ simulationConfig);
+ }
+
private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) {
return () ->
new FragmentedMp4Extractor(
diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java
index 992a6fdad0d..0059a9d99af 100644
--- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java
+++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java
@@ -69,6 +69,15 @@
* {@link MediaController#COMMAND_SEEK_TO_NEXT} to seek to the next item.
*
*
+ * Custom commands
+ *
+ * Custom actions are sent to the session under the hood. You can receive them by overriding the
+ * session callback method {@link MediaSession.Callback#onCustomCommand(MediaSession,
+ * MediaSession.ControllerInfo, SessionCommand, Bundle)}. This is useful because starting with
+ * Android 13, the System UI notification sends commands directly to the session. So handling the
+ * custom commands on the session level allows you to handle them at the same callback for all API
+ * levels.
+ *
* Drawables
*
* The drawables used can be overridden by drawables with the same names defined the application.
@@ -219,6 +228,14 @@ public final boolean handleCustomCommand(MediaSession session, String action, Bu
* customized by defining the index of the command in compact view of up to 3 commands in their
* extras with key {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX}.
*
+ * To make the custom layout and commands work, you need to {@linkplain
+ * MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom
+ * commands to the available commands when a controller {@linkplain
+ * MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the
+ * session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)}
+ * need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession,
+ * MediaSession.ControllerInfo)} also.
+ *
* @param playerCommands The available player commands.
* @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of
* commands}.
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump
new file mode 100644
index 00000000000..5b9a721cb6f
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump
@@ -0,0 +1,339 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 18257
+ sample count = 46
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 18, hash 96519432
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 4, hash EE9DF
+ sample 2:
+ time = 46439
+ flags = 1
+ data = length 4, hash EEDBF
+ sample 3:
+ time = 69659
+ flags = 1
+ data = length 157, hash E2F078F4
+ sample 4:
+ time = 92879
+ flags = 1
+ data = length 371, hash B9471F94
+ sample 5:
+ time = 116099
+ flags = 1
+ data = length 373, hash 2AB265CB
+ sample 6:
+ time = 139319
+ flags = 1
+ data = length 402, hash 1295477C
+ sample 7:
+ time = 162539
+ flags = 1
+ data = length 455, hash 2D8146C8
+ sample 8:
+ time = 185759
+ flags = 1
+ data = length 434, hash F2C5D287
+ sample 9:
+ time = 208979
+ flags = 1
+ data = length 450, hash 84143FCD
+ sample 10:
+ time = 232199
+ flags = 1
+ data = length 429, hash EF769D50
+ sample 11:
+ time = 255419
+ flags = 1
+ data = length 450, hash EC3DE692
+ sample 12:
+ time = 278639
+ flags = 1
+ data = length 447, hash 3E519E13
+ sample 13:
+ time = 301859
+ flags = 1
+ data = length 457, hash 1E4F23A0
+ sample 14:
+ time = 325079
+ flags = 1
+ data = length 447, hash A439EA97
+ sample 15:
+ time = 348299
+ flags = 1
+ data = length 456, hash 1E9034C6
+ sample 16:
+ time = 371519
+ flags = 1
+ data = length 398, hash 99DB7345
+ sample 17:
+ time = 394739
+ flags = 1
+ data = length 474, hash 3F05F10A
+ sample 18:
+ time = 417959
+ flags = 1
+ data = length 416, hash C105EE09
+ sample 19:
+ time = 441179
+ flags = 1
+ data = length 454, hash 5FDBE458
+ sample 20:
+ time = 464399
+ flags = 1
+ data = length 438, hash 41A93AC3
+ sample 21:
+ time = 487619
+ flags = 1
+ data = length 443, hash 10FDA652
+ sample 22:
+ time = 510839
+ flags = 1
+ data = length 412, hash 1F791E25
+ sample 23:
+ time = 534058
+ flags = 1
+ data = length 482, hash A6D983D
+ sample 24:
+ time = 557278
+ flags = 1
+ data = length 386, hash BED7392F
+ sample 25:
+ time = 580498
+ flags = 1
+ data = length 463, hash 5309F8C9
+ sample 26:
+ time = 603718
+ flags = 1
+ data = length 394, hash 21C7321F
+ sample 27:
+ time = 626938
+ flags = 1
+ data = length 489, hash 71B4730D
+ sample 28:
+ time = 650158
+ flags = 1
+ data = length 403, hash D9C6DE89
+ sample 29:
+ time = 673378
+ flags = 1
+ data = length 447, hash 9B14B73B
+ sample 30:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 31:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 32:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 33:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 34:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 35:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 36:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 37:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 38:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 39:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 40:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 41:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 42:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 43:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 44:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 45:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump
new file mode 100644
index 00000000000..53cb776780d
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump
@@ -0,0 +1,279 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 13359
+ sample count = 31
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 348299
+ flags = 1
+ data = length 456, hash 1E9034C6
+ sample 1:
+ time = 371519
+ flags = 1
+ data = length 398, hash 99DB7345
+ sample 2:
+ time = 394739
+ flags = 1
+ data = length 474, hash 3F05F10A
+ sample 3:
+ time = 417959
+ flags = 1
+ data = length 416, hash C105EE09
+ sample 4:
+ time = 441179
+ flags = 1
+ data = length 454, hash 5FDBE458
+ sample 5:
+ time = 464399
+ flags = 1
+ data = length 438, hash 41A93AC3
+ sample 6:
+ time = 487619
+ flags = 1
+ data = length 443, hash 10FDA652
+ sample 7:
+ time = 510839
+ flags = 1
+ data = length 412, hash 1F791E25
+ sample 8:
+ time = 534058
+ flags = 1
+ data = length 482, hash A6D983D
+ sample 9:
+ time = 557278
+ flags = 1
+ data = length 386, hash BED7392F
+ sample 10:
+ time = 580498
+ flags = 1
+ data = length 463, hash 5309F8C9
+ sample 11:
+ time = 603718
+ flags = 1
+ data = length 394, hash 21C7321F
+ sample 12:
+ time = 626938
+ flags = 1
+ data = length 489, hash 71B4730D
+ sample 13:
+ time = 650158
+ flags = 1
+ data = length 403, hash D9C6DE89
+ sample 14:
+ time = 673378
+ flags = 1
+ data = length 447, hash 9B14B73B
+ sample 15:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 16:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 17:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 18:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 19:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 20:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 21:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 22:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 23:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 24:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 25:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 26:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 27:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 28:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 29:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 30:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump
new file mode 100644
index 00000000000..ecb83ddeeaa
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump
@@ -0,0 +1,219 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 6804
+ sample count = 16
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 1:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 2:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 3:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 4:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 5:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 6:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 7:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 8:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 9:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 10:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 11:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 12:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 13:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 14:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 15:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump
new file mode 100644
index 00000000000..c0498099406
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump
@@ -0,0 +1,159 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 10
+ sample count = 1
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump
new file mode 100644
index 00000000000..5b9a721cb6f
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump
@@ -0,0 +1,339 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 18257
+ sample count = 46
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 18, hash 96519432
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 4, hash EE9DF
+ sample 2:
+ time = 46439
+ flags = 1
+ data = length 4, hash EEDBF
+ sample 3:
+ time = 69659
+ flags = 1
+ data = length 157, hash E2F078F4
+ sample 4:
+ time = 92879
+ flags = 1
+ data = length 371, hash B9471F94
+ sample 5:
+ time = 116099
+ flags = 1
+ data = length 373, hash 2AB265CB
+ sample 6:
+ time = 139319
+ flags = 1
+ data = length 402, hash 1295477C
+ sample 7:
+ time = 162539
+ flags = 1
+ data = length 455, hash 2D8146C8
+ sample 8:
+ time = 185759
+ flags = 1
+ data = length 434, hash F2C5D287
+ sample 9:
+ time = 208979
+ flags = 1
+ data = length 450, hash 84143FCD
+ sample 10:
+ time = 232199
+ flags = 1
+ data = length 429, hash EF769D50
+ sample 11:
+ time = 255419
+ flags = 1
+ data = length 450, hash EC3DE692
+ sample 12:
+ time = 278639
+ flags = 1
+ data = length 447, hash 3E519E13
+ sample 13:
+ time = 301859
+ flags = 1
+ data = length 457, hash 1E4F23A0
+ sample 14:
+ time = 325079
+ flags = 1
+ data = length 447, hash A439EA97
+ sample 15:
+ time = 348299
+ flags = 1
+ data = length 456, hash 1E9034C6
+ sample 16:
+ time = 371519
+ flags = 1
+ data = length 398, hash 99DB7345
+ sample 17:
+ time = 394739
+ flags = 1
+ data = length 474, hash 3F05F10A
+ sample 18:
+ time = 417959
+ flags = 1
+ data = length 416, hash C105EE09
+ sample 19:
+ time = 441179
+ flags = 1
+ data = length 454, hash 5FDBE458
+ sample 20:
+ time = 464399
+ flags = 1
+ data = length 438, hash 41A93AC3
+ sample 21:
+ time = 487619
+ flags = 1
+ data = length 443, hash 10FDA652
+ sample 22:
+ time = 510839
+ flags = 1
+ data = length 412, hash 1F791E25
+ sample 23:
+ time = 534058
+ flags = 1
+ data = length 482, hash A6D983D
+ sample 24:
+ time = 557278
+ flags = 1
+ data = length 386, hash BED7392F
+ sample 25:
+ time = 580498
+ flags = 1
+ data = length 463, hash 5309F8C9
+ sample 26:
+ time = 603718
+ flags = 1
+ data = length 394, hash 21C7321F
+ sample 27:
+ time = 626938
+ flags = 1
+ data = length 489, hash 71B4730D
+ sample 28:
+ time = 650158
+ flags = 1
+ data = length 403, hash D9C6DE89
+ sample 29:
+ time = 673378
+ flags = 1
+ data = length 447, hash 9B14B73B
+ sample 30:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 31:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 32:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 33:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 34:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 35:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 36:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 37:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 38:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 39:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 40:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 41:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 42:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 43:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 44:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 45:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4
new file mode 100644
index 00000000000..39fd4c18cf2
Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 differ
diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url
new file mode 100644
index 00000000000..ed362b729a4
--- /dev/null
+++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ https://testserver1.test/AcquireLicense
+
+
+
+
+
+ https://testserver2.test/AcquireLicense
+
+
+
+
+
+
diff --git a/libraries/test_data/src/test/assets/playbackdumps/mp4/sample_fragmented_large_bitrates.mp4.dump b/libraries/test_data/src/test/assets/playbackdumps/mp4/sample_fragmented_large_bitrates.mp4.dump
new file mode 100644
index 00000000000..b8f6657598e
--- /dev/null
+++ b/libraries/test_data/src/test/assets/playbackdumps/mp4/sample_fragmented_large_bitrates.mp4.dump
@@ -0,0 +1,82 @@
+MediaCodecAdapter (exotest.audio.aac):
+ buffers.length = 47
+ buffers[0] = length 18, hash 96519432
+ buffers[1] = length 4, hash EE9DF
+ buffers[2] = length 4, hash EEDBF
+ buffers[3] = length 157, hash E2F078F4
+ buffers[4] = length 371, hash B9471F94
+ buffers[5] = length 373, hash 2AB265CB
+ buffers[6] = length 402, hash 1295477C
+ buffers[7] = length 455, hash 2D8146C8
+ buffers[8] = length 434, hash F2C5D287
+ buffers[9] = length 450, hash 84143FCD
+ buffers[10] = length 429, hash EF769D50
+ buffers[11] = length 450, hash EC3DE692
+ buffers[12] = length 447, hash 3E519E13
+ buffers[13] = length 457, hash 1E4F23A0
+ buffers[14] = length 447, hash A439EA97
+ buffers[15] = length 456, hash 1E9034C6
+ buffers[16] = length 398, hash 99DB7345
+ buffers[17] = length 474, hash 3F05F10A
+ buffers[18] = length 416, hash C105EE09
+ buffers[19] = length 454, hash 5FDBE458
+ buffers[20] = length 438, hash 41A93AC3
+ buffers[21] = length 443, hash 10FDA652
+ buffers[22] = length 412, hash 1F791E25
+ buffers[23] = length 482, hash A6D983D
+ buffers[24] = length 386, hash BED7392F
+ buffers[25] = length 463, hash 5309F8C9
+ buffers[26] = length 394, hash 21C7321F
+ buffers[27] = length 489, hash 71B4730D
+ buffers[28] = length 403, hash D9C6DE89
+ buffers[29] = length 447, hash 9B14B73B
+ buffers[30] = length 439, hash 4760D35B
+ buffers[31] = length 463, hash 1601F88D
+ buffers[32] = length 423, hash D4AE6773
+ buffers[33] = length 497, hash A3C674D3
+ buffers[34] = length 419, hash D3734A1F
+ buffers[35] = length 474, hash DFB41F9
+ buffers[36] = length 413, hash 53E7CB9F
+ buffers[37] = length 445, hash D15B0E39
+ buffers[38] = length 453, hash 77ED81E4
+ buffers[39] = length 545, hash 3321AEB9
+ buffers[40] = length 317, hash F557D0E
+ buffers[41] = length 537, hash ED58CF7B
+ buffers[42] = length 458, hash 51CDAA10
+ buffers[43] = length 465, hash CBA1EFD7
+ buffers[44] = length 446, hash D6735B8A
+ buffers[45] = length 10, hash A453EEBE
+ buffers[46] = length 0, hash 1
+MediaCodecAdapter (exotest.video.avc):
+ buffers.length = 31
+ buffers[0] = length 38070, hash B58E1AEE
+ buffers[1] = length 8340, hash 8AC449FF
+ buffers[2] = length 1295, hash C0DA5090
+ buffers[3] = length 469, hash D6E0A200
+ buffers[4] = length 564, hash E5F56C5B
+ buffers[5] = length 6075, hash 8756E49E
+ buffers[6] = length 847, hash DCC2B618
+ buffers[7] = length 455, hash B9CCE047
+ buffers[8] = length 467, hash 69806D94
+ buffers[9] = length 4549, hash 3944F501
+ buffers[10] = length 1087, hash 491BF106
+ buffers[11] = length 380, hash 5FED016A
+ buffers[12] = length 455, hash 8A0610
+ buffers[13] = length 5190, hash B9031D8
+ buffers[14] = length 1071, hash 684E7DC8
+ buffers[15] = length 653, hash 8494F326
+ buffers[16] = length 485, hash 2CCC85F4
+ buffers[17] = length 4884, hash D16B6A96
+ buffers[18] = length 997, hash 164FF210
+ buffers[19] = length 640, hash F664125B
+ buffers[20] = length 491, hash B5930C7C
+ buffers[21] = length 2989, hash 92CF4FCF
+ buffers[22] = length 838, hash 294A3451
+ buffers[23] = length 544, hash FCCE2DE6
+ buffers[24] = length 329, hash A654FFA1
+ buffers[25] = length 1517, hash 5F7EBF8B
+ buffers[26] = length 803, hash 7A5C4C1D
+ buffers[27] = length 415, hash B31BBC3B
+ buffers[28] = length 415, hash 850DFEA3
+ buffers[29] = length 619, hash AB5E56CA
+ buffers[30] = length 0, hash 1
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/Action.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/Action.java
index f025836ba24..cf5f420c6a1 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/Action.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/Action.java
@@ -15,6 +15,8 @@
*/
package androidx.media3.test.utils;
+import static androidx.media3.test.utils.TestUtil.timelinesAreSame;
+
import android.os.Looper;
import android.view.Surface;
import androidx.annotation.Nullable;
@@ -765,7 +767,7 @@ public WaitForTimelineChanged(
@Nullable Timeline expectedTimeline,
@Player.TimelineChangeReason int expectedReason) {
super(tag, "WaitForTimelineChanged");
- this.expectedTimeline = expectedTimeline != null ? new NoUidTimeline(expectedTimeline) : null;
+ this.expectedTimeline = expectedTimeline;
this.ignoreExpectedReason = false;
this.expectedReason = expectedReason;
}
@@ -797,7 +799,7 @@ protected void doActionAndScheduleNextImpl(
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
- if ((expectedTimeline == null || new NoUidTimeline(timeline).equals(expectedTimeline))
+ if ((expectedTimeline == null || timelinesAreSame(timeline, expectedTimeline))
&& (ignoreExpectedReason || expectedReason == reason)) {
player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler);
@@ -805,8 +807,8 @@ public void onTimelineChanged(
}
};
player.addListener(listener);
- Timeline currentTimeline = new NoUidTimeline(player.getCurrentTimeline());
- if (currentTimeline.equals(expectedTimeline)) {
+ if (expectedTimeline != null
+ && timelinesAreSame(player.getCurrentTimeline(), expectedTimeline)) {
player.removeListener(listener);
nextAction.schedule(player, trackSelector, surface, handler);
}
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/ExoPlayerTestRunner.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/ExoPlayerTestRunner.java
index 3bf7bc3866c..0cb7b4d9353 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/ExoPlayerTestRunner.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/ExoPlayerTestRunner.java
@@ -43,6 +43,7 @@
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -536,11 +537,8 @@ public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs)
* @param timelines A list of expected {@link Timeline}s.
*/
public void assertTimelinesSame(Timeline... timelines) {
- assertThat(this.timelines).hasSize(timelines.length);
- for (int i = 0; i < timelines.length; i++) {
- assertThat(new NoUidTimeline(timelines[i]))
- .isEqualTo(new NoUidTimeline(this.timelines.get(i)));
- }
+ TestUtil.assertTimelinesSame(
+ ImmutableList.copyOf(this.timelines), ImmutableList.copyOf(timelines));
}
/**
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTimeline.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTimeline.java
index 75e3d26e7b9..9aad788a7a7 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTimeline.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeTimeline.java
@@ -29,6 +29,7 @@
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.source.ShuffleOrder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
@@ -275,7 +276,7 @@ public TimelineWindowDefinition(
private final TimelineWindowDefinition[] windowDefinitions;
private final Object[] manifests;
private final int[] periodOffsets;
- private final FakeShuffleOrder fakeShuffleOrder;
+ private final ShuffleOrder shuffleOrder;
/**
* Returns an ad playback state with the specified number of ads in each of the specified ad
@@ -395,6 +396,19 @@ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
* @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
*/
public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) {
+ this(manifests, new FakeShuffleOrder(windowDefinitions.length), windowDefinitions);
+ }
+
+ /**
+ * Creates a fake timeline with the given window definitions and {@link
+ * androidx.media3.exoplayer.source.ShuffleOrder}.
+ *
+ * @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
+ */
+ public FakeTimeline(
+ Object[] manifests,
+ ShuffleOrder shuffleOrder,
+ TimelineWindowDefinition... windowDefinitions) {
this.manifests = new Object[windowDefinitions.length];
System.arraycopy(manifests, 0, this.manifests, 0, min(this.manifests.length, manifests.length));
this.windowDefinitions = windowDefinitions;
@@ -403,7 +417,7 @@ public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefini
for (int i = 0; i < windowDefinitions.length; i++) {
periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount;
}
- fakeShuffleOrder = new FakeShuffleOrder(windowDefinitions.length);
+ this.shuffleOrder = shuffleOrder;
}
@Override
@@ -422,7 +436,7 @@ public int getNextWindowIndex(
? getFirstWindowIndex(shuffleModeEnabled)
: C.INDEX_UNSET;
}
- return shuffleModeEnabled ? fakeShuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
+ return shuffleModeEnabled ? shuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
}
@Override
@@ -436,20 +450,20 @@ public int getPreviousWindowIndex(
? getLastWindowIndex(shuffleModeEnabled)
: C.INDEX_UNSET;
}
- return shuffleModeEnabled ? fakeShuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
+ return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
}
@Override
public int getLastWindowIndex(boolean shuffleModeEnabled) {
return shuffleModeEnabled
- ? fakeShuffleOrder.getLastIndex()
+ ? shuffleOrder.getLastIndex()
: super.getLastWindowIndex(/* shuffleModeEnabled= */ false);
}
@Override
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
return shuffleModeEnabled
- ? fakeShuffleOrder.getFirstIndex()
+ ? shuffleOrder.getFirstIndex()
: super.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
}
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/NoUidTimeline.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/NoUidTimeline.java
deleted file mode 100644
index 5cd0adad0c4..00000000000
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/NoUidTimeline.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2019 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 androidx.media3.test.utils;
-
-import androidx.media3.common.Timeline;
-import androidx.media3.common.util.UnstableApi;
-import androidx.media3.exoplayer.source.ForwardingTimeline;
-
-/**
- * A timeline which wraps another timeline and overrides all window and period uids to 0. This is
- * useful for testing timeline equality without taking uids into account.
- */
-@UnstableApi
-public class NoUidTimeline extends ForwardingTimeline {
-
- /**
- * Creates an instance.
- *
- * @param timeline The underlying timeline.
- */
- public NoUidTimeline(Timeline timeline) {
- super(timeline);
- }
-
- @Override
- public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
- timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
- window.uid = 0;
- return window;
- }
-
- @Override
- public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- timeline.getPeriod(periodIndex, period, setIds);
- period.uid = 0;
- return period;
- }
-}
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java
index 85569df265e..97c7d3cf4a2 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubPlayer.java
@@ -70,6 +70,7 @@ public void removeListener(Listener listener) {
}
@Override
+ @Nullable
public PlaybackException getPlayerError() {
throw new UnsupportedOperationException();
}
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java
index a59377c27dc..27872827fa5 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java
@@ -44,6 +44,7 @@
import androidx.media3.extractor.metadata.MetadataInputBuffer;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Bytes;
+import com.google.common.truth.Correspondence;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -199,19 +200,33 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
/**
* Asserts that the actual timelines are the same to the expected timelines. This assert differs
- * from testing equality by not comparing period ids which may be different due to id mapping of
- * child source period ids.
+ * from testing equality by not comparing:
+ *
+ *
+ * - Period IDs, which may be different due to ID mapping of child source period IDs.
+ *
- Shuffle order, which by default is random and non-deterministic.
+ *
*
* @param actualTimelines A list of actual {@link Timeline timelines}.
* @param expectedTimelines A list of expected {@link Timeline timelines}.
*/
public static void assertTimelinesSame(
List actualTimelines, List expectedTimelines) {
- assertThat(actualTimelines).hasSize(expectedTimelines.size());
- for (int i = 0; i < actualTimelines.size(); i++) {
- assertThat(new NoUidTimeline(actualTimelines.get(i)))
- .isEqualTo(new NoUidTimeline(expectedTimelines.get(i)));
- }
+ assertThat(actualTimelines)
+ .comparingElementsUsing(
+ Correspondence.from(
+ TestUtil::timelinesAreSame, "is equal to (ignoring Window.uid and Period.uid)"))
+ .containsExactlyElementsIn(expectedTimelines)
+ .inOrder();
+ }
+
+ /**
+ * Returns true if {@code thisTimeline} is equal to {@code thatTimeline}, ignoring {@link
+ * Timeline.Window#uid} and {@link Timeline.Period#uid} values, and shuffle order.
+ */
+ public static boolean timelinesAreSame(Timeline thisTimeline, Timeline thatTimeline) {
+ return new NoUidOrShufflingTimeline(thisTimeline)
+ .equals(new NoUidOrShufflingTimeline(thatTimeline));
}
/**
@@ -494,4 +509,68 @@ public static List getPublicMethods(Class> clazz) {
return list;
}
+
+ private static final class NoUidOrShufflingTimeline extends Timeline {
+
+ private final Timeline delegate;
+
+ public NoUidOrShufflingTimeline(Timeline timeline) {
+ this.delegate = timeline;
+ }
+
+ @Override
+ public int getWindowCount() {
+ return delegate.getWindowCount();
+ }
+
+ @Override
+ public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
+ return delegate.getNextWindowIndex(windowIndex, repeatMode, /* shuffleModeEnabled= */ false);
+ }
+
+ @Override
+ public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
+ return delegate.getPreviousWindowIndex(
+ windowIndex, repeatMode, /* shuffleModeEnabled= */ false);
+ }
+
+ @Override
+ public int getLastWindowIndex(boolean shuffleModeEnabled) {
+ return delegate.getLastWindowIndex(/* shuffleModeEnabled= */ false);
+ }
+
+ @Override
+ public int getFirstWindowIndex(boolean shuffleModeEnabled) {
+ return delegate.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ delegate.getWindow(windowIndex, window, defaultPositionProjectionUs);
+ window.uid = 0;
+ return window;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return delegate.getPeriodCount();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ delegate.getPeriod(periodIndex, period, setIds);
+ period.uid = 0;
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return delegate.getIndexOfPeriod(uid);
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ return 0;
+ }
+ }
}
diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java
index 29c3d09c57b..658e9f56bec 100644
--- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java
+++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java
@@ -184,41 +184,6 @@ public static ExoPlaybackException runUntilError(ExoPlayer player) throws Timeou
return checkNotNull(player.getPlayerError());
}
- /**
- * Runs tasks of the main {@link Looper} until {@link
- * ExoPlayer.AudioOffloadListener#onExperimentalOffloadSchedulingEnabledChanged} is called or a
- * playback error occurs.
- *
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
- *
- * @param player The {@link Player}.
- * @return The new offloadSchedulingEnabled state.
- * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
- * exceeded.
- */
- public static boolean runUntilReceiveOffloadSchedulingEnabledNewState(ExoPlayer player)
- throws TimeoutException {
- verifyMainTestThread(player);
- AtomicReference<@NullableType Boolean> offloadSchedulingEnabledReceiver =
- new AtomicReference<>();
- ExoPlayer.AudioOffloadListener listener =
- new ExoPlayer.AudioOffloadListener() {
- @Override
- public void onExperimentalOffloadSchedulingEnabledChanged(
- boolean offloadSchedulingEnabled) {
- offloadSchedulingEnabledReceiver.set(offloadSchedulingEnabled);
- }
- };
- player.addAudioOffloadListener(listener);
- runMainLooperUntil(
- () -> offloadSchedulingEnabledReceiver.get() != null || player.getPlayerError() != null);
- player.removeAudioOffloadListener(listener);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
- }
- return checkNotNull(offloadSchedulingEnabledReceiver.get());
- }
-
/**
* Runs tasks of the main {@link Looper} until {@link
* ExoPlayer.AudioOffloadListener#onExperimentalSleepingForOffloadChanged(boolean)} is called or a
diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java
index 7ab349bd1dd..8ec6d274983 100644
--- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java
+++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java
@@ -1811,7 +1811,13 @@ public void onBindViewHolder(SubSettingViewHolder holder, int position) {
if (position < playbackSpeedTexts.length) {
holder.textView.setText(playbackSpeedTexts[position]);
}
- holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
+ if (position == selectedIndex) {
+ holder.itemView.setSelected(true);
+ holder.checkView.setVisibility(VISIBLE);
+ } else {
+ holder.itemView.setSelected(false);
+ holder.checkView.setVisibility(INVISIBLE);
+ }
holder.itemView.setOnClickListener(
v -> {
if (position != selectedIndex) {
diff --git a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java
index 77d25ce9dcb..84a8c9eb75b 100644
--- a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java
+++ b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java
@@ -236,11 +236,6 @@ public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
// Player.Listener implementation.
- @Override
- public void onPlaybackStateChanged(@Player.State int playbackState) {
- notifyStateChanged();
- }
-
@Override
public void onPlayerError(PlaybackException error) {
Callback callback = getCallback();
@@ -285,5 +280,13 @@ public void onVideoSizeChanged(VideoSize videoSize) {
int scaledWidth = Math.round(videoSize.width * videoSize.pixelWidthHeightRatio);
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, scaledWidth, videoSize.height);
}
+
+ @Override
+ public void onEvents(Player player, Player.Events events) {
+ if (events.containsAny(
+ Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED)) {
+ notifyStateChanged();
+ }
+ }
}
}