Commit ca6835bf by Rohit Kumar Singh Committed by GitHub

Merge pull request #120 from androidx/release-1.0

r1.0.0 beta02
parents 2c720102 86e2361c
Showing with 2809 additions and 414 deletions
......@@ -17,6 +17,7 @@ body:
label: Media3 Version
description: What version of Media3 are you using?
options:
- 1.0.0-beta02
- 1.0.0-beta01
- 1.0.0-alpha03
- 1.0.0-alpha02
......
Release notes
### Unreleased changes
### 1.0.0-beta02 (2022-07-21)
This release corresponds to the
[ExoPlayer 2.18.1 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.1).
* Core library:
* Ensure that changing the `ShuffleOrder` with `ExoPlayer.setShuffleOrder`
results in a call to `Player.Listener#onTimelineChanged` with
`reason=Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED`
([#9889](https://github.com/google/ExoPlayer/issues/9889)).
* For progressive media, only include selected tracks in buffered position
([#10361](https://github.com/google/ExoPlayer/issues/10361)).
* Allow custom logger for all ExoPlayer log output
([#9752](https://github.com/google/ExoPlayer/issues/9752)).
* Fix implementation of `setDataSourceFactory` in
`DefaultMediaSourceFactory`, which was non-functional in some cases
([#116](https://github.com/androidx/media/issues/116)).
* Extractors:
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* Fix parsing of H265 short term reference picture sets
([#10316](https://github.com/google/ExoPlayer/issues/10316)).
* Fix parsing of bitrates from `esds` boxes
([#10381](https://github.com/google/ExoPlayer/issues/10381)).
* DASH:
* Parse ClearKey license URL from manifests
([#10246](https://github.com/google/ExoPlayer/issues/10246)).
* UI:
* Ensure TalkBack announces the currently active speed option in the
playback controls menu
([#10298](https://github.com/google/ExoPlayer/issues/10298)).
* RTSP:
* Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)).
* Add VP8 fragmented packet handling
([#110](https://github.com/androidx/media/pull/110)).
* Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)).
* Cast:
* Use the `MediaItem` that has been passed to the playlist methods as
`Window.mediaItem` in `CastTimeline`
([#25](https://github.com/androidx/media/issues/25),
[#8212](https://github.com/google/ExoPlayer/issues/8212)).
* Support `Player.getMetadata()` and `Listener.onMediaMetadataChanged()`
with `CastPlayer` ([#25](https://github.com/androidx/media/issues/25)).
### 1.0.0-beta01 (2022-06-16)
......@@ -46,7 +80,9 @@ This release corresponds to the
* Rename `TracksInfo` to `Tracks` and `TracksInfo.TrackGroupInfo` to
`Tracks.Group`. `Player.getCurrentTracksInfo` and
`Player.Listener.onTracksInfoChanged` have also been renamed to
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`.
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`. This
includes 'un-deprecating' the `Player.Listener.onTracksChanged` method
name, but with different parameter types.
* Change `DefaultTrackSelector.buildUponParameters` and
`DefaultTrackSelector.Parameters.buildUpon` to return
`DefaultTrackSelector.Parameters.Builder` instead of the deprecated
......@@ -100,6 +136,8 @@ This release corresponds to the
* Remove `RawCcExtractor`, which was only used to handle a Google-internal
subtitle format.
* Extractors:
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* Matroska: Parse `DiscardPadding` for Opus tracks.
* MP4: Parse bitrates from `esds` boxes.
* Ogg: Allow duplicate Opus ID and comment headers
......@@ -149,6 +187,8 @@ This release corresponds to the
of `DefaultCompositeSequenceableLoaderFactory` can be passed explicitly
if required.
* RTSP:
* Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)).
* Add RTP reader for MPEG4
([#35](https://github.com/androidx/media/pull/35)).
* Add RTP reader for HEVC
......@@ -211,10 +251,11 @@ This release corresponds to the
AndroidStudio's gradle sync to fail
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* Remove deprecated symbols:
* Remove `Player.Listener.onTracksChanged`. Use
`Player.Listener.onTracksInfoChanged` instead.
* Remove `Player.Listener.onTracksChanged(TrackGroupArray,
TrackSelectionArray)`. Use `Player.Listener.onTracksChanged(Tracks)`
instead.
* Remove `Player.getCurrentTrackGroups` and
`Player.getCurrentTrackSelections`. Use `Player.getCurrentTracksInfo`
`Player.getCurrentTrackSelections`. Use `Player.getCurrentTracks`
instead. You can also continue to use `ExoPlayer.getCurrentTrackGroups`
and `ExoPlayer.getCurrentTrackSelections`, although these methods remain
deprecated.
......@@ -424,7 +465,7 @@ This release corresponds to the
when creating `PendingIntent`s
([#9528](https://github.com/google/ExoPlayer/issues/9528)).
* Remove deprecated symbols:
* Remove `Player.EventLister`. Use `Player.Listener` instead.
* Remove `Player.EventListener`. Use `Player.Listener` instead.
* Remove `MediaSourceFactory.setDrmSessionManager`,
`MediaSourceFactory.setDrmHttpDataSourceFactory`, and
`MediaSourceFactory.setDrmUserAgent`. Use
......
......@@ -807,7 +807,7 @@ package androidx.media3.common {
field public static final int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; // 0x1
}
@IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command {
@IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command {
}
public static final class Player.Commands {
......
......@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.0.0-beta01'
releaseVersionCode = 1_000_000_1_01
releaseVersion = '1.0.0-beta02'
releaseVersionCode = 1_000_000_1_02
minSdkVersion = 16
appTargetSdkVersion = 29
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
......
......@@ -79,6 +79,12 @@
<data android:scheme="ssai"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.main.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.main.action.VIEW_LIST"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
......
......@@ -27,6 +27,7 @@ import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaBrowser
......@@ -164,7 +165,7 @@ class MainActivity : AppCompatActivity() {
val root: MediaItem = result.value!!
pushPathStack(root)
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
}
......
......@@ -29,6 +29,7 @@ import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaBrowser
......@@ -150,7 +151,7 @@ class PlayableFolderActivity : AppCompatActivity() {
val result = mediaItemFuture.get()!!
title.text = result.value!!.mediaMetadata.title
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
childrenFuture.addListener(
{
......@@ -161,7 +162,7 @@ class PlayableFolderActivity : AppCompatActivity() {
subItemMediaList.addAll(children)
mediaListAdapter.notifyDataSetChanged()
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
}
......
......@@ -43,6 +43,12 @@
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.surface.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
</application>
......
......@@ -49,6 +49,12 @@
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
<intent-filter>
<action android:name="androidx.media3.demo.transformer.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".TransformerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
......
......@@ -101,9 +101,9 @@ public final class CastPlayer extends BasePlayer {
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_SET_MEDIA_ITEM)
COMMAND_GET_TRACKS)
.build();
public static final float MIN_SPEED_SUPPORTED = 0.5f;
......@@ -145,6 +145,7 @@ public final class CastPlayer extends BasePlayer {
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs;
@Nullable private PositionInfo pendingMediaItemRemovalPosition;
private MediaMetadata mediaMetadata;
/**
* Creates a new cast player.
......@@ -198,7 +199,7 @@ public final class CastPlayer extends BasePlayer {
this.mediaItemConverter = mediaItemConverter;
this.seekBackIncrementMs = seekBackIncrementMs;
this.seekForwardIncrementMs = seekForwardIncrementMs;
timelineTracker = new CastTimelineTracker();
timelineTracker = new CastTimelineTracker(mediaItemConverter);
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
......@@ -212,6 +213,7 @@ public final class CastPlayer extends BasePlayer {
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
mediaMetadata = MediaMetadata.EMPTY;
currentTracks = Tracks.EMPTY;
availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET;
......@@ -283,8 +285,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
setMediaItemsInternal(
toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value);
setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value);
}
@Override
......@@ -294,7 +295,7 @@ public final class CastPlayer extends BasePlayer {
if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
}
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
addMediaItemsInternal(mediaItems, uid);
}
@Override
......@@ -426,6 +427,13 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_MEDIA_ITEM_TRANSITION,
listener ->
listener.onMediaItemTransition(mediaItem, MEDIA_ITEM_TRANSITION_REASON_SEEK));
MediaMetadata oldMediaMetadata = mediaMetadata;
mediaMetadata = getMediaMetadataInternal();
if (!oldMediaMetadata.equals(mediaMetadata)) {
listeners.queueEvent(
Player.EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
}
updateAvailableCommandsAndNotifyIfChanged();
} else if (pendingSeekCount == 0) {
......@@ -563,8 +571,12 @@ public final class CastPlayer extends BasePlayer {
@Override
public MediaMetadata getMediaMetadata() {
// CastPlayer does not currently support metadata.
return MediaMetadata.EMPTY;
return mediaMetadata;
}
public MediaMetadata getMediaMetadataInternal() {
MediaItem currentMediaItem = getCurrentMediaItem();
return currentMediaItem != null ? currentMediaItem.mediaMetadata : MediaMetadata.EMPTY;
}
@Override
......@@ -761,6 +773,7 @@ public final class CastPlayer extends BasePlayer {
return;
}
int oldWindowIndex = this.currentWindowIndex;
MediaMetadata oldMediaMetadata = mediaMetadata;
@Nullable
Object oldPeriodUid =
!getCurrentTimeline().isEmpty()
......@@ -772,6 +785,7 @@ public final class CastPlayer extends BasePlayer {
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
Timeline currentTimeline = getCurrentTimeline();
currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
mediaMetadata = getMediaMetadataInternal();
@Nullable
Object currentPeriodUid =
!currentTimeline.isEmpty()
......@@ -825,6 +839,11 @@ public final class CastPlayer extends BasePlayer {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks));
}
if (!oldMediaMetadata.equals(mediaMetadata)) {
listeners.queueEvent(
Player.EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents();
}
......@@ -1020,14 +1039,13 @@ public final class CastPlayer extends BasePlayer {
}
}
@Nullable
private PendingResult<MediaChannelResult> setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems,
private void setMediaItemsInternal(
List<MediaItem> mediaItems,
int startIndex,
long startPositionMs,
@RepeatMode int repeatMode) {
if (remoteMediaClient == null || mediaQueueItems.length == 0) {
return null;
if (remoteMediaClient == null || mediaItems.isEmpty()) {
return;
}
startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
if (startIndex == C.INDEX_UNSET) {
......@@ -1038,34 +1056,35 @@ public final class CastPlayer extends BasePlayer {
if (!currentTimeline.isEmpty()) {
pendingMediaItemRemovalPosition = getCurrentPositionInfo();
}
return remoteMediaClient.queueLoad(
MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems);
remoteMediaClient.queueLoad(
mediaQueueItems,
min(startIndex, mediaQueueItems.length - 1),
min(startIndex, mediaItems.size() - 1),
getCastRepeatMode(repeatMode),
startPositionMs,
/* customData= */ null);
}
@Nullable
private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
private void addMediaItemsInternal(List<MediaItem> mediaItems, int uid) {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
return;
}
return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null);
MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems);
timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert);
remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null);
}
@Nullable
private PendingResult<MediaChannelResult> moveMediaItemsInternal(
int[] uids, int fromIndex, int newIndex) {
private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
return;
}
int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
if (insertBeforeIndex < currentTimeline.getWindowCount()) {
insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
}
return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
}
@Nullable
......
......@@ -15,13 +15,13 @@
*/
package androidx.media3.cast;
import android.net.Uri;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import com.google.android.gms.cast.MediaInfo;
import java.util.Arrays;
/** A {@link Timeline} for Cast media queues. */
......@@ -30,12 +30,16 @@ import java.util.Arrays;
/** Holds {@link Timeline} related data for a Cast media item. */
public static final class ItemData {
/* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID";
/** Holds no media information. */
public static final ItemData EMPTY =
new ItemData(
/* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ C.TIME_UNSET,
/* isLive= */ false);
/* isLive= */ false,
MediaItem.EMPTY,
UNKNOWN_CONTENT_ID);
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public final long durationUs;
......@@ -45,6 +49,10 @@ import java.util.Arrays;
public final long defaultPositionUs;
/** Whether the item is live content, or {@code false} if unknown. */
public final boolean isLive;
/** The original media item that has been set or added to the playlist. */
public final MediaItem mediaItem;
/** The {@linkplain MediaInfo#getContentId() content ID} of the cast media queue item. */
public final String contentId;
/**
* Creates an instance.
......@@ -52,11 +60,20 @@ import java.util.Arrays;
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}.
* @param mediaItem See {@link #mediaItem}.
* @param contentId See {@link #contentId}.
*/
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData(
long durationUs,
long defaultPositionUs,
boolean isLive,
MediaItem mediaItem,
String contentId) {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive;
this.mediaItem = mediaItem;
this.contentId = contentId;
}
/**
......@@ -66,14 +83,23 @@ import java.util.Arrays;
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
* if unknown.
* @param isLive Whether the item is live, or {@code false} if unknown.
* @param mediaItem The media item.
* @param contentId The content ID.
*/
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData copyWithNewValues(
long durationUs,
long defaultPositionUs,
boolean isLive,
MediaItem mediaItem,
String contentId) {
if (durationUs == this.durationUs
&& defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) {
&& isLive == this.isLive
&& contentId.equals(this.contentId)
&& mediaItem.equals(this.mediaItem)) {
return this;
}
return new ItemData(durationUs, defaultPositionUs, isLive);
return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId);
}
}
......@@ -82,6 +108,7 @@ import java.util.Arrays;
new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex;
private final MediaItem[] mediaItems;
private final int[] ids;
private final long[] durationsUs;
private final long[] defaultPositionsUs;
......@@ -100,10 +127,12 @@ import java.util.Arrays;
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
isLive = new boolean[itemCount];
mediaItems = new MediaItem[itemCount];
for (int i = 0; i < ids.length; i++) {
int id = ids[i];
idsToIndex.put(id, i);
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
mediaItems[i] = data.mediaItem;
durationsUs[i] = data.durationUs;
defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
isLive[i] = data.isLive;
......@@ -121,18 +150,16 @@ import java.util.Arrays;
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
MediaItem mediaItem =
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
return window.set(
/* uid= */ ids[windowIndex],
/* mediaItem= */ mediaItem,
/* mediaItem= */ mediaItems[windowIndex],
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
isDynamic,
isLive[windowIndex] ? mediaItem.liveConfiguration : null,
isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null,
defaultPositionsUs[windowIndex],
durationUs,
/* firstPeriodIndex= */ windowIndex,
......
......@@ -15,14 +15,23 @@
*/
package androidx.media3.cast;
import static androidx.media3.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
......@@ -33,9 +42,47 @@ import java.util.HashSet;
/* package */ final class CastTimelineTracker {
private final SparseArray<CastTimeline.ItemData> itemIdToData;
private final MediaItemConverter mediaItemConverter;
@VisibleForTesting /* package */ final HashMap<String, MediaItem> mediaItemsByContentId;
public CastTimelineTracker() {
/**
* Creates an instance.
*
* @param mediaItemConverter The converter used to convert from a {@link MediaQueueItem} to a
* {@link MediaItem}.
*/
public CastTimelineTracker(MediaItemConverter mediaItemConverter) {
this.mediaItemConverter = mediaItemConverter;
itemIdToData = new SparseArray<>();
mediaItemsByContentId = new HashMap<>();
}
/**
* Called when media items {@linkplain Player#setMediaItems have been set to the playlist} and are
* sent to the cast playback queue. A future queue update of the {@link RemoteMediaClient} will
* reflect this addition.
*
* @param mediaItems The media items that have been set.
* @param mediaQueueItems The corresponding media queue items.
*/
public void onMediaItemsSet(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
mediaItemsByContentId.clear();
onMediaItemsAdded(mediaItems, mediaQueueItems);
}
/**
* Called when media items {@linkplain Player#addMediaItems(List) have been added} and are sent to
* the cast playback queue. A future queue update of the {@link RemoteMediaClient} will reflect
* this addition.
*
* @param mediaItems The media items that have been added.
* @param mediaQueueItems The corresponding media queue items.
*/
public void onMediaItemsAdded(List<MediaItem> mediaItems, MediaQueueItem[] mediaQueueItems) {
for (int i = 0; i < mediaItems.size(); i++) {
mediaItemsByContentId.put(
checkNotNull(mediaQueueItems[i].getMedia()).getContentId(), mediaItems.get(i));
}
}
/**
......@@ -63,18 +110,36 @@ import java.util.HashSet;
}
int currentItemId = mediaStatus.getCurrentItemId();
String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId();
MediaItem mediaItem = mediaItemsByContentId.get(currentContentId);
updateItemData(
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
currentItemId,
mediaItem != null ? mediaItem : MediaItem.EMPTY,
mediaStatus.getMediaInfo(),
currentContentId,
/* defaultPositionUs= */ C.TIME_UNSET);
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND);
@Nullable MediaInfo mediaInfo = queueItem.getMedia();
String contentId = mediaInfo != null ? mediaInfo.getContentId() : UNKNOWN_CONTENT_ID;
mediaItem = mediaItemsByContentId.get(contentId);
updateItemData(
queueItem.getItemId(),
mediaItem != null ? mediaItem : mediaItemConverter.toMediaItem(queueItem),
mediaInfo,
contentId,
defaultPositionUs);
}
return new CastTimeline(itemIds, itemIdToData);
}
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
private void updateItemData(
int itemId,
MediaItem mediaItem,
@Nullable MediaInfo mediaInfo,
String contentId,
long defaultPositionUs) {
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
if (durationUs == C.TIME_UNSET) {
......@@ -87,7 +152,10 @@ import java.util.HashSet;
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs;
}
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
itemIdToData.put(
itemId,
previousData.copyWithNewValues(
durationUs, defaultPositionUs, isLive, mediaItem, contentId));
}
private void removeUnusedItemDataEntries(int[] itemIds) {
......@@ -99,6 +167,8 @@ import java.util.HashSet;
int index = 0;
while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
CastTimeline.ItemData itemData = itemIdToData.valueAt(index);
mediaItemsByContentId.remove(itemData.contentId);
itemIdToData.removeAt(index);
} else {
index++;
......
......@@ -128,11 +128,14 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
if (mediaItem.mediaMetadata.trackNumber != null) {
metadata.putInt(MediaMetadata.KEY_TRACK_NUMBER, mediaItem.mediaMetadata.trackNumber);
}
String contentUrl = mediaItem.localConfiguration.uri.toString();
String contentId =
mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? contentUrl : mediaItem.mediaId;
MediaInfo mediaInfo =
new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString())
new MediaInfo.Builder(contentId)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(mediaItem.localConfiguration.mimeType)
.setContentUrl(contentUrl)
.setMetadata(metadata)
.setCustomData(getCustomData(mediaItem))
.build();
......
......@@ -50,6 +50,7 @@ public class DefaultMediaItemConverterTest {
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 class DefaultMediaItemConverterTest {
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");
}
}
......@@ -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')
}
......
......@@ -18,6 +18,7 @@ package androidx.media3.common;
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 final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @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 final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @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);
}
......
......@@ -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;
......
......@@ -373,6 +373,7 @@ public interface Player {
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 interface Player {
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
};
private final FlagSet.Builder flagsBuilder;
......@@ -1432,6 +1432,7 @@ public interface Player {
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 @@ public interface Player {
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 @@ public interface Player {
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 @@ public interface Player {
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;
......
......@@ -1351,6 +1351,27 @@ public abstract class Timeline implements Bundleable {
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 abstract class Timeline implements Bundleable {
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;
}
......
......@@ -18,6 +18,7 @@ package androidx.media3.common.util;
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.lang.annotation.Target;
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.
*
* <p>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 @@ public final class 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 final class Log {
* @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 final class Log {
*/
@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 final class Log {
*/
@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 final class Log {
*/
@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 final class Log {
*/
@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 final class Log {
@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", " ");
}
}
}
......
......@@ -55,6 +55,7 @@ import android.os.Handler;
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 final class Util {
@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 final class Util {
* @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 final class Util {
*/
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<MediaItem.SubtitleConfiguration> 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<String> 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 final class Util {
* @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 final class Util {
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 final class Util {
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 final class Util {
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 final class Util {
/** 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 final class Util {
*/
@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 final class Util {
@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 final class Util {
// 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 final class Util {
}
// 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 @@ public final class Util {
@RequiresApi(24)
private static String[] getSystemLocalesV24(Configuration config) {
return Util.split(config.getLocales().toLanguageTags(), ",");
return split(config.getLocales().toLanguageTags(), ",");
}
@RequiresApi(21)
......
......@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
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;
......@@ -65,6 +66,50 @@ public class TimelineTest {
}
@Test
public void timelineEquals() {
ImmutableList<TimelineWindowDefinition> 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<TimelineWindowDefinition> 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();
Timeline.Window window = new Timeline.Window();
......
......@@ -294,6 +294,7 @@ import java.util.concurrent.TimeoutException;
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 @@ import java.util.concurrent.TimeoutException;
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 @@ import java.util.concurrent.TimeoutException;
public void experimentalSetOffloadSchedulingEnabled(boolean offloadSchedulingEnabled) {
verifyApplicationThread();
internalPlayer.experimentalSetOffloadSchedulingEnabled(offloadSchedulingEnabled);
for (AudioOffloadListener listener : audioOffloadListeners) {
listener.onExperimentalOffloadSchedulingEnabledChanged(offloadSchedulingEnabled);
}
}
@Override
......@@ -707,6 +710,7 @@ import java.util.concurrent.TimeoutException;
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
verifyApplicationThread();
this.shuffleOrder = shuffleOrder;
Timeline timeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
......@@ -715,7 +719,6 @@ import java.util.concurrent.TimeoutException;
maskWindowPositionMsOrGetPeriodPositionUs(
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
pendingOperationAcks++;
this.shuffleOrder = shuffleOrder;
internalPlayer.setShuffleOrder(shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
......@@ -1962,12 +1965,6 @@ import java.util.concurrent.TimeoutException;
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);
......
......@@ -817,10 +817,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
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 @@ import java.util.concurrent.atomic.AtomicBoolean;
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 @@ import java.util.concurrent.atomic.AtomicBoolean;
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 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
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 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* bufferedPositionUs= */ startPositionUs,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ startPositionUs,
offloadSchedulingEnabled,
/* sleepingForOffload= */ false);
if (releaseMediaSourceList) {
mediaSourceList.release();
......
......@@ -74,8 +74,6 @@ import java.util.List;
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 @@ import java.util.List;
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
/* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
}
......@@ -145,7 +142,6 @@ import java.util.List;
* @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 @@ import java.util.List;
long bufferedPositionUs,
long totalBufferedDurationUs,
long positionUs,
boolean offloadSchedulingEnabled,
boolean sleepingForOffload) {
this.timeline = timeline;
this.periodId = periodId;
......@@ -185,7 +180,6 @@ import java.util.List;
this.bufferedPositionUs = bufferedPositionUs;
this.totalBufferedDurationUs = totalBufferedDurationUs;
this.positionUs = positionUs;
this.offloadSchedulingEnabled = offloadSchedulingEnabled;
this.sleepingForOffload = sleepingForOffload;
}
......@@ -237,7 +231,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -267,7 +260,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -297,7 +289,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -327,7 +318,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -357,7 +347,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -387,7 +376,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -421,7 +409,6 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
......@@ -451,38 +438,6 @@ import java.util.List;
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 @@ import java.util.List;
bufferedPositionUs,
totalBufferedDurationUs,
positionUs,
offloadSchedulingEnabled,
sleepingForOffload);
}
}
......@@ -29,7 +29,6 @@ import android.media.AudioManager;
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.Format;
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 @@ public final class DefaultAudioSink implements AudioSink {
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 @@ public final class DefaultAudioSink implements AudioSink {
}
}
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 @@ public final class DefaultAudioSink implements AudioSink {
}
startMediaTimeUsNeedsInit = true;
return true;
}
@Override
......@@ -930,7 +935,10 @@ public final class DefaultAudioSink implements AudioSink {
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.
......
......@@ -317,7 +317,9 @@ public final class MediaCodecInfo {
}
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 @@ public final class MediaCodecInfo {
}
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));
}
}
......@@ -282,6 +282,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
*/
public DefaultMediaSourceFactory setDataSourceFactory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
delegateFactoryLoader.setDataSourceFactory(dataSourceFactory);
return this;
}
......@@ -594,6 +595,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
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 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
@Nullable Supplier<MediaSource.Factory> mediaSourceFactorySupplier = null;
DataSource.Factory dataSourceFactory = checkNotNull(this.dataSourceFactory);
try {
Class<? extends MediaSource.Factory> clazz;
switch (contentType) {
......@@ -634,19 +637,19 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
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 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
break;
case C.CONTENT_TYPE_OTHER:
mediaSourceFactorySupplier =
() ->
new ProgressiveMediaSource.Factory(
checkNotNull(dataSourceFactory), extractorsFactory);
() -> new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory);
break;
default:
// Do nothing.
......
......@@ -39,7 +39,7 @@ public interface TextOutput {
* Called when there is a change in the {@link CueGroup}.
*
* <p>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);
}
......@@ -629,7 +629,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
return surface != null || shouldUseDummySurface(codecInfo);
return surface != null || shouldUsePlaceholderSurface(codecInfo);
}
@Override
......@@ -706,7 +706,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeNotifyRenderedFirstFrame();
}
private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) {
private boolean shouldUsePlaceholderSurface(MediaCodecInfo codecInfo) {
return Util.SDK_INT >= 23
&& !tunneling
&& !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name)
......@@ -1572,7 +1572,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
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 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
*
* @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;
......
......@@ -53,13 +53,13 @@ import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.o
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.MediaPeriod;
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.FakeTimeline.TimelineWindowDefinition;
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;
......@@ -6513,6 +6513,53 @@ public final class ExoPlayerTest {
}
@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<Timeline> timelineCaptor = ArgumentCaptor.forClass(Timeline.class);
verify(mockListener)
.onTimelineChanged(
timelineCaptor.capture(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
Timeline capturedTimeline = Iterables.getOnlyElement(timelineCaptor.getAllValues());
List<Integer> newShuffleOrderIndexes = new ArrayList<>(newShuffleOrder.getLength());
for (int i = newShuffleOrder.getFirstIndex();
i != C.INDEX_UNSET;
i = newShuffleOrder.getNextIndex(i)) {
newShuffleOrderIndexes.add(i);
}
List<Integer> 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};
ActionSchedule actionSchedule =
......@@ -9635,47 +9682,16 @@ public final class ExoPlayerTest {
}
@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 final class ExoPlayerTest {
* Returns an argument matcher for {@link Timeline} instances that ignores period and window uids.
*/
private static ArgumentMatcher<Timeline> noUid(Timeline timeline) {
return argument -> new NoUidTimeline(timeline).equals(new NoUidTimeline(argument));
return argument -> timelinesAreSame(argument, timeline);
}
}
......@@ -1112,7 +1112,6 @@ public final class MediaPeriodQueueTest {
/* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0,
/* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
}
......
......@@ -57,6 +57,7 @@ public class Mp4PlaybackTest {
"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",
......
......@@ -599,6 +599,9 @@ public class DashManifestParser extends DefaultHandler
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 @@ public class DashManifestParser extends DefaultHandler
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 @@ public class DashManifestParser extends DefaultHandler
ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
fillInClearKeyInformation(drmSchemeDatas);
filterRedundantIncompleteSchemeDatas(drmSchemeDatas);
formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas));
}
......@@ -1660,6 +1666,32 @@ public class DashManifestParser extends DefaultHandler
}
}
private static void fillInClearKeyInformation(ArrayList<SchemeData> 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.
*
......
......@@ -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 class DefaultMediaSourceFactoryTest {
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;
}
});
}
}
......@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
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 class DashManifestParserTest {
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<Descriptor> buildCea608AccessibilityDescriptors(String value) {
return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null));
}
......
......@@ -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 class DefaultMediaSourceFactoryTest {
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;
}
});
}
}
......@@ -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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return true;
}
/**
* Outputs sample metadata of the received fragmented packets.
*
* <p>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
......
/*
* 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);
}
}
......@@ -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
}
......
......@@ -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 class DefaultMediaSourceFactoryTest {
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;
}
});
}
}
......@@ -18,6 +18,7 @@ package androidx.media3.extractor;
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 @@ public final class NalUnitUtil {
}
}
/**
* Skips any short term reference picture sets contained in a SPS.
*
* <p>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;
}
}
......
......@@ -45,6 +45,7 @@ import androidx.media3.extractor.OpusUtil;
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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
.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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
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;
......
......@@ -170,6 +170,32 @@ public final class NalUnitUtilTest {
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++) {
......
......@@ -122,6 +122,15 @@ public final class FragmentedMp4ExtractorTest {
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<Format> closedCaptionFormats) {
return () ->
new FragmentedMp4Extractor(
......
......@@ -69,6 +69,15 @@ import java.util.concurrent.ExecutionException;
* <li>{@link MediaController#COMMAND_SEEK_TO_NEXT} to seek to the next item.
* </ul>
*
* <h2>Custom commands</h2>
*
* 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.
*
* <h2>Drawables</h2>
*
* The drawables used can be overridden by drawables with the same names defined the application.
......@@ -219,6 +228,14 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
* 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}.
*
* <p>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}.
......
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
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
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
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
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
<?xml version="1.0" encoding="UTF-8"?>
<!--
Includes ContentProtection elements with additional ClearKey license URLs.
Covers all possible locations (in AdaptationSet and Representation) and possible orders of these
ContentProtection elements (CENC first or ClearKey first).
-->
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:DASH:schema:MPD:2011" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="static" availabilityStartTime="2016-10-14T17:00:17" xmlns:cenc="urn:mpeg:cenc:2013" xmlns:clearkey="http://dashif.org/guidelines/clearKey">
<Period start="PT0.000S" duration="PT0H5M50S">
<SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
<SegmentTimeline>
<S d="2002" t="6009" r="2"/>
</SegmentTimeline>
</SegmentTemplate>
<AdaptationSet id="0" mimeType="audio/mp4" subsegmentAlignment="true">
<Representation id="140" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="144000">
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="9eb4050d-e44b-4802-932e-27d75083e266" />
<ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
<clearkey:Laurl Lic_type="EME-1.0">https://testserver1.test/AcquireLicense</clearkey:Laurl>
</ContentProtection>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true">
<ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
<clearkey:Laurl Lic_type="EME-1.0">https://testserver2.test/AcquireLicense</clearkey:Laurl>
</ContentProtection>
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="9eb4050d-e44b-4802-932e-27d75083e266" />
<Representation id="133" codecs="avc1.4d4015" width="426" height="240" startWithSAP="1" bandwidth="258000" frameRate="30" />
</AdaptationSet>
</Period>
</MPD>
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
......@@ -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 abstract class Action {
@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 @@ public abstract class Action {
@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 abstract class Action {
}
};
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);
}
......
......@@ -43,6 +43,7 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener;
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 final class ExoPlayerTestRunner implements Player.Listener, ActionSchedul
* @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));
}
/**
......
......@@ -29,6 +29,7 @@ import androidx.media3.common.Timeline;
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 final class FakeTimeline extends Timeline {
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 final class FakeTimeline extends Timeline {
* @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 final class FakeTimeline extends Timeline {
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 final class FakeTimeline extends Timeline {
? 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 final class FakeTimeline extends Timeline {
? 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);
}
......
/*
* 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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment