/*
* Copyright 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.session;
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Command;
import androidx.media3.common.Player.Commands;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.Util;
import androidx.media3.session.PlayerInfo.BundlingExclusions;
import java.util.ArrayList;
import java.util.List;
/* package */ final class MediaUtils {
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 256 * 1024; // 256KB
/** Constant to identify whether two calculated positions are considered as same */
public static final long POSITION_DIFF_TOLERANCE_MS = 100;
private static final String TAG = "MediaUtils";
// Stub BrowserRoot for accepting any connection here.
public static final BrowserRoot defaultBrowserRoot =
new BrowserRoot(MediaLibraryService.SERVICE_INTERFACE, null);
/** Returns whether two {@link PlaybackStateCompat} have equal error. */
public static boolean areEqualError(
@Nullable PlaybackStateCompat a, @Nullable PlaybackStateCompat b) {
boolean aHasError = a != null && a.getState() == PlaybackStateCompat.STATE_ERROR;
boolean bHasError = b != null && b.getState() == PlaybackStateCompat.STATE_ERROR;
if (aHasError && bHasError) {
return castNonNull(a).getErrorCode() == castNonNull(b).getErrorCode()
&& TextUtils.equals(castNonNull(a).getErrorMessage(), castNonNull(b).getErrorMessage());
}
return aHasError == bHasError;
}
/**
* Returns a list which consists of first {@code N} items of the given list with the same order.
* {@code N} is determined as the maximum number of items whose total parcelled size is less than
* {@code sizeLimitInBytes}.
*/
public static <T extends Parcelable> List<T> truncateListBySize(
List<T> list, int sizeLimitInBytes) {
List<T> result = new ArrayList<>();
Parcel parcel = Parcel.obtain();
try {
for (int i = 0; i < list.size(); i++) {
// Calculate the size.
T item = list.get(i);
parcel.writeParcelable(item, 0);
if (parcel.dataSize() < sizeLimitInBytes) {
result.add(item);
} else {
break;
}
}
} finally {
parcel.recycle();
}
return result;
}
/** Returns a new list that only contains non-null elements of the original list. */
public static <T> List<T> removeNullElements(List<@NullableType T> list) {
List<T> newList = new ArrayList<>();
for (@Nullable T item : list) {
if (item != null) {
newList.add(item);
}
}
return newList;
}
public static Commands createPlayerCommandsWith(@Command int command) {
return new Commands.Builder().add(command).build();
}
public static Commands createPlayerCommandsWithout(@Command int command) {
return new Commands.Builder().addAllCommands().remove(command).build();
}
/**
* Returns the intersection of {@link Player.Command commands} from the given two {@link
* Commands}.
*/
public static Commands intersect(@Nullable Commands commands1, @Nullable Commands commands2) {
if (commands1 == null || commands2 == null) {
return Commands.EMPTY;
}
Commands.Builder intersectCommandsBuilder = new Commands.Builder();
for (int i = 0; i < commands1.size(); i++) {
if (commands2.contains(commands1.get(i))) {
intersectCommandsBuilder.add(commands1.get(i));
}
}
return intersectCommandsBuilder.build();
}
/**
* Merges the excluded fields into the {@code newPlayerInfo} by taking the values of the {@code
* previousPlayerInfo} and taking into account the passed available commands.
*
* @param oldPlayerInfo The old {@link PlayerInfo}.
* @param oldBundlingExclusions The bundling exclusions in the old {@link PlayerInfo}.
* @param newPlayerInfo The new {@link PlayerInfo}.
* @param newBundlingExclusions The bundling exclusions in the new {@link PlayerInfo}.
* @param availablePlayerCommands The available commands to take into account when merging.
* @return A pair with the resulting {@link PlayerInfo} and {@link BundlingExclusions}.
*/
public static Pair<PlayerInfo, BundlingExclusions> mergePlayerInfo(
PlayerInfo oldPlayerInfo,
BundlingExclusions oldBundlingExclusions,
PlayerInfo newPlayerInfo,
BundlingExclusions newBundlingExclusions,
Commands availablePlayerCommands) {
PlayerInfo mergedPlayerInfo = newPlayerInfo;
BundlingExclusions mergedBundlingExclusions = newBundlingExclusions;
if (newBundlingExclusions.isTimelineExcluded
&& availablePlayerCommands.contains(Player.COMMAND_GET_TIMELINE)
&& !oldBundlingExclusions.isTimelineExcluded) {
// Use the previous timeline if it is excluded in the most recent update.
mergedPlayerInfo = mergedPlayerInfo.copyWithTimeline(oldPlayerInfo.timeline);
mergedBundlingExclusions =
new BundlingExclusions(
/* isTimelineExcluded= */ false, mergedBundlingExclusions.areCurrentTracksExcluded);
}
if (newBundlingExclusions.areCurrentTracksExcluded
&& availablePlayerCommands.contains(Player.COMMAND_GET_TRACKS)
&& !oldBundlingExclusions.areCurrentTracksExcluded) {
// Use the previous tracks if it is excluded in the most recent update.
mergedPlayerInfo = mergedPlayerInfo.copyWithCurrentTracks(oldPlayerInfo.currentTracks);
mergedBundlingExclusions =
new BundlingExclusions(
mergedBundlingExclusions.isTimelineExcluded, /* areCurrentTracksExcluded= */ false);
}
return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions);
}
/** Generates an array of {@code n} indices. */
public static int[] generateUnshuffledIndices(int n) {
int[] indices = new int[n];
for (int i = 0; i < n; i++) {
indices[i] = i;
}
return indices;
}
/**
* Calculates the buffered percentage of the given buffered position and the duration in
* milliseconds.
*/
public static int calculateBufferedPercentage(long bufferedPositionMs, long durationMs) {
return bufferedPositionMs == C.TIME_UNSET || durationMs == C.TIME_UNSET
? 0
: durationMs == 0
? 100
: Util.constrainValue((int) ((bufferedPositionMs * 100) / durationMs), 0, 100);
}
/**
* Sets media items with start index and position for the given {@link Player} by honoring the
* available commands.
*
* @param player The player to set the media items.
* @param mediaItemsWithStartPosition The media items, the index and the position to set.
*/
public static void setMediaItemsWithStartIndexAndPosition(
Player player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) {
if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET) {
if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true);
} else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) {
player.setMediaItem(
mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true);
}
} else if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
player.setMediaItems(
mediaItemsWithStartPosition.mediaItems,
mediaItemsWithStartPosition.startIndex,
mediaItemsWithStartPosition.startPositionMs);
} else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) {
player.setMediaItem(
mediaItemsWithStartPosition.mediaItems.get(0),
mediaItemsWithStartPosition.startPositionMs);
}
}
/**
* Returns whether the two provided {@link SessionPositionInfo} describe a position in the same
* period or ad.
*/
public static boolean areSessionPositionInfosInSamePeriodOrAd(
SessionPositionInfo info1, SessionPositionInfo info2) {
// TODO: b/259220235 - Use UIDs instead of mediaItemIndex and periodIndex
return info1.positionInfo.mediaItemIndex == info2.positionInfo.mediaItemIndex
&& info1.positionInfo.periodIndex == info2.positionInfo.periodIndex
&& info1.positionInfo.adGroupIndex == info2.positionInfo.adGroupIndex
&& info1.positionInfo.adIndexInAdGroup == info2.positionInfo.adIndexInAdGroup;
}
/**
* Returns updated value for a media controller position estimate.
*
* @param playerInfo The current {@link PlayerInfo}.
* @param currentPositionMs The current known position estimate in milliseconds, or {@link
* C#TIME_UNSET} if still unknown.
* @param lastSetPlayWhenReadyCalledTimeMs The {@link SystemClock#elapsedRealtime()} when the
* controller was last used to call {@link MediaController#setPlayWhenReady}, or {@link
* C#TIME_UNSET} if it was never called.
* @param timeDiffMs A time difference override since the last {@link PlayerInfo} update. Should
* be {@link C#TIME_UNSET} except for testing.
* @return The updated position estimate in milliseconds.
*/
public static long getUpdatedCurrentPositionMs(
PlayerInfo playerInfo,
long currentPositionMs,
long lastSetPlayWhenReadyCalledTimeMs,
long timeDiffMs) {
boolean receivedUpdatedPositionInfo =
lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs;
if (!playerInfo.isPlaying) {
if (receivedUpdatedPositionInfo || currentPositionMs == C.TIME_UNSET) {
return playerInfo.sessionPositionInfo.positionInfo.positionMs;
} else {
return currentPositionMs;
}
}
if (!receivedUpdatedPositionInfo && currentPositionMs != C.TIME_UNSET) {
// Need an updated current position in order to make a new position estimation
return currentPositionMs;
}
long elapsedTimeMs =
timeDiffMs != C.TIME_UNSET
? timeDiffMs
: SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs;
long estimatedPositionMs =
playerInfo.sessionPositionInfo.positionInfo.positionMs
+ (long) (elapsedTimeMs * playerInfo.playbackParameters.speed);
if (playerInfo.sessionPositionInfo.durationMs != C.TIME_UNSET) {
estimatedPositionMs = min(estimatedPositionMs, playerInfo.sessionPositionInfo.durationMs);
}
return estimatedPositionMs;
}
private MediaUtils() {}
}