| /* |
| * 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.media2.player; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_BAD_VALUE; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_INVALID_STATE; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_IO; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_PERMISSION_DENIED; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_ERROR_UNKNOWN; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_INFO_SKIPPED; |
| import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS; |
| |
| import android.content.Context; |
| import android.graphics.SurfaceTexture; |
| import android.media.AudioManager; |
| import android.media.DeniedByServerException; |
| import android.media.MediaDrm; |
| import android.media.MediaDrmException; |
| import android.media.MediaFormat; |
| import android.os.PersistableBundle; |
| import android.util.Log; |
| import android.view.Surface; |
| |
| import androidx.annotation.FloatRange; |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.IntRange; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.RestrictTo; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.collection.ArrayMap; |
| import androidx.concurrent.ListenableFuture; |
| import androidx.concurrent.callback.AbstractResolvableFuture; |
| import androidx.concurrent.callback.ResolvableFuture; |
| import androidx.core.util.Pair; |
| import androidx.media.AudioAttributesCompat; |
| import androidx.media2.common.FileMediaItem; |
| import androidx.media2.common.MediaItem; |
| import androidx.media2.common.MediaMetadata; |
| import androidx.media2.common.SessionPlayer; |
| import androidx.media2.common.SubtitleData; |
| import androidx.media2.common.UriMediaItem; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.UUID; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| /** |
| * A media player which plays {@link MediaItem}s. The details on playback control and player states |
| * can be found in the documentation of the base class, {@link SessionPlayer}. |
| * <p> |
| * Topic covered here: |
| * <ol> |
| * <li><a href="#AudioFocusAndNoisyIntent">Audio focus and noisy intent</a> |
| * </ol> |
| * <a name="AudioFocusAndNoisyIntent"></a> |
| * <h3>Audio focus and noisy intent</h3> |
| * <p> |
| * By default, {@link MediaPlayer} handles audio focus and noisy intent with |
| * {@link AudioAttributesCompat} set to this player. You need to call |
| * {@link #setAudioAttributes(AudioAttributesCompat)} set the audio attribute while in the |
| * {@link #PLAYER_STATE_IDLE}. |
| * <p> |
| * Here's the table of automatic audio focus behavior with audio attributes. |
| * <table> |
| * <tr><th>Audio Attributes</th><th>Audio Focus Gain Type</th><th>Misc</th></tr> |
| * <tr><td>{@link AudioAttributesCompat#USAGE_VOICE_COMMUNICATION_SIGNALLING}</td> |
| * <td>{@link android.media.AudioManager#AUDIOFOCUS_NONE}</td> |
| * <td /></tr> |
| * <tr><td><ul><li>{@link AudioAttributesCompat#USAGE_GAME}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_MEDIA}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_UNKNOWN}</li></ul></td> |
| * <td>{@link android.media.AudioManager#AUDIOFOCUS_GAIN}</td> |
| * <td>Developers should specific a proper usage instead of |
| * {@link AudioAttributesCompat#USAGE_UNKNOWN}</td></tr> |
| * <tr><td><ul><li>{@link AudioAttributesCompat#USAGE_ALARM}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_VOICE_COMMUNICATION}</li></ul></td> |
| * <td>{@link android.media.AudioManager#AUDIOFOCUS_GAIN_TRANSIENT}</td> |
| * <td /></tr> |
| * <tr><td><ul><li>{@link AudioAttributesCompat#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_ASSISTANCE_SONIFICATION}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_NOTIFICATION}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_NOTIFICATION_COMMUNICATION_DELAYED}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_NOTIFICATION_COMMUNICATION_INSTANT}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_NOTIFICATION_COMMUNICATION_REQUEST}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_NOTIFICATION_EVENT}</li> |
| * <li>{@link AudioAttributesCompat#USAGE_NOTIFICATION_RINGTONE}</li></ul></td> |
| * <td>{@link android.media.AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}</td> |
| * <td /></tr> |
| * <tr><td><ul><li>{@link AudioAttributesCompat#USAGE_ASSISTANT}</li></ul></td> |
| * <td>{@link android.media.AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}</td> |
| * <td /></tr> |
| * <tr><td>{@link AudioAttributesCompat#USAGE_ASSISTANCE_ACCESSIBILITY}</td> |
| * <td>{@link android.media.AudioManager#AUDIOFOCUS_GAIN_TRANSIENT} if |
| * {@link AudioAttributesCompat#CONTENT_TYPE_SPEECH}, |
| * {@link android.media.AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} otherwise</td> |
| * <td /></tr> |
| * <tr><td>{@code null}</td> |
| * <td>No audio focus handling, and sets the player volume to {@code 0}</td> |
| * <td>Only valid if your media contents don't have audio</td></tr> |
| * <tr><td>Any other AudioAttributes</td> |
| * <td>No audio focus handling, and sets the player volume to {@code 0}</td> |
| * <td>This is to handle error</td></tr> |
| * </table> |
| * <p> |
| * If an {@link AudioAttributesCompat} is not specified by {@link #setAudioAttributes}, |
| * {@link #getAudioAttributes} will return {@code null} and the default audio focus behavior will |
| * follow the {@code null} case on the table above. |
| * <p> |
| * For more information about the audio focus, take a look at |
| * <a href="{@docRoot}guide/topics/media-apps/audio-focus.html">Managing audio focus</a> |
| * <p> |
| */ |
| public final class MediaPlayer extends SessionPlayer { |
| private static final String TAG = "MediaPlayer"; |
| |
| /** |
| * Unspecified player error. |
| * @see PlayerCallback#onError |
| */ |
| public static final int PLAYER_ERROR_UNKNOWN = 1; |
| /** |
| * File or network related operation errors. |
| * @see PlayerCallback#onError |
| */ |
| public static final int PLAYER_ERROR_IO = -1004; |
| /** |
| * Bitstream is not conforming to the related coding standard or file spec. |
| * @see PlayerCallback#onError |
| */ |
| public static final int PLAYER_ERROR_MALFORMED = -1007; |
| /** |
| * Bitstream is conforming to the related coding standard or file spec, but |
| * the media framework does not support the feature. |
| * @see PlayerCallback#onError |
| */ |
| public static final int PLAYER_ERROR_UNSUPPORTED = -1010; |
| /** |
| * Some operation takes too long to complete, usually more than 3-5 seconds. |
| * @see PlayerCallback#onError |
| */ |
| public static final int PLAYER_ERROR_TIMED_OUT = -110; |
| |
| /** |
| * @hide |
| */ |
| @IntDef(flag = false, /*prefix = "PLAYER_ERROR",*/ value = { |
| PLAYER_ERROR_UNKNOWN, |
| PLAYER_ERROR_IO, |
| PLAYER_ERROR_MALFORMED, |
| PLAYER_ERROR_UNSUPPORTED, |
| PLAYER_ERROR_TIMED_OUT, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public @interface MediaError {} |
| |
| /** |
| * The player just started the playback of this media item. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_MEDIA_ITEM_START = 2; |
| |
| /** |
| * The player just pushed the very first video frame for rendering. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; |
| |
| /** |
| * The player just completed the playback of this media item. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_MEDIA_ITEM_END = 5; |
| |
| /** |
| * The player just completed the playback of all the media items set by {@link #setPlaylist} |
| * and {@link #setMediaItem}. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_MEDIA_ITEM_LIST_END = 6; |
| |
| /** |
| * The player just completed an iteration of playback loop. This event is sent only when |
| * looping is enabled by {@link #setRepeatMode(int)}. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_MEDIA_ITEM_REPEAT = 7; |
| |
| /** |
| * The player just finished preparing a media item for playback. |
| * @see #prepare() |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_PREPARED = 100; |
| |
| /** |
| * The video is too complex for the decoder: it can't decode frames fast |
| * enough. Possibly only the audio plays fine at this stage. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; |
| |
| /** |
| * The player is temporarily pausing playback internally in order to |
| * buffer more data. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_BUFFERING_START = 701; |
| |
| /** |
| * The player is resuming playback after filling buffers. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_BUFFERING_END = 702; |
| |
| /** |
| * Estimated network bandwidth information (kbps) is available; currently this event fires |
| * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END} |
| * when playing network files. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703; |
| |
| /** |
| * Update status in buffering a media source received through progressive downloading. |
| * The received buffering percentage indicates how much of the content has been buffered |
| * or played. For example a buffering update of 80 percent when half the content |
| * has already been played indicates that the next 30 percent of the |
| * content to play has been buffered. |
| * |
| * <p>The {@code extra} parameter in {@link PlayerCallback#onInfo} is the |
| * percentage (0-100) of the content that has been buffered or played thus far. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; |
| |
| /** |
| * Bad interleaving means that a media has been improperly interleaved or |
| * not interleaved at all, e.g has all the video samples first then all the |
| * audio ones. Video is playing but a lot of disk seeks may be happening. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; |
| |
| /** |
| * The media cannot be seeked (e.g live stream) |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_NOT_SEEKABLE = 801; |
| |
| /** |
| * A new set of metadata is available. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_METADATA_UPDATE = 802; |
| |
| /** |
| * A new set of external-only metadata is available. Used by |
| * JAVA framework to avoid triggering track scanning. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_EXTERNAL_METADATA_UPDATE = 803; |
| |
| /** |
| * Informs that audio is not playing. Note that playback of the video |
| * is not interrupted. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; |
| |
| /** |
| * Informs that video is not playing. Note that playback of the audio |
| * is not interrupted. |
| * @see PlayerCallback#onInfo |
| */ |
| public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; |
| |
| /** |
| * Subtitle track was not supported by the media framework. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901; |
| |
| /** |
| * Reading the subtitle track takes too long. |
| * @see PlayerCallback#onInfo |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902; |
| |
| /** |
| * @hide |
| */ |
| @IntDef(flag = false, /*prefix = "MEDIA_INFO",*/ value = { |
| MEDIA_INFO_MEDIA_ITEM_START, |
| MEDIA_INFO_VIDEO_RENDERING_START, |
| MEDIA_INFO_MEDIA_ITEM_END, |
| MEDIA_INFO_MEDIA_ITEM_LIST_END, |
| MEDIA_INFO_MEDIA_ITEM_REPEAT, |
| MEDIA_INFO_PREPARED, |
| MEDIA_INFO_VIDEO_TRACK_LAGGING, |
| MEDIA_INFO_BUFFERING_START, |
| MEDIA_INFO_BUFFERING_END, |
| MEDIA_INFO_NETWORK_BANDWIDTH, |
| MEDIA_INFO_BUFFERING_UPDATE, |
| MEDIA_INFO_BAD_INTERLEAVING, |
| MEDIA_INFO_NOT_SEEKABLE, |
| MEDIA_INFO_METADATA_UPDATE, |
| MEDIA_INFO_EXTERNAL_METADATA_UPDATE, |
| MEDIA_INFO_AUDIO_NOT_PLAYING, |
| MEDIA_INFO_VIDEO_NOT_PLAYING, |
| MEDIA_INFO_UNSUPPORTED_SUBTITLE, |
| MEDIA_INFO_SUBTITLE_TIMED_OUT |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public @interface MediaInfo {} |
| |
| /** |
| * This mode is used with {@link #seekTo(long, int)} to move media position to |
| * a sync (or key) frame associated with a media item that is located |
| * right before or at the given time. |
| * |
| * @see #seekTo(long, int) |
| */ |
| public static final int SEEK_PREVIOUS_SYNC = 0x00; |
| /** |
| * This mode is used with {@link #seekTo(long, int)} to move media position to |
| * a sync (or key) frame associated with a media item that is located |
| * right after or at the given time. |
| * |
| * @see #seekTo(long, int) |
| */ |
| public static final int SEEK_NEXT_SYNC = 0x01; |
| /** |
| * This mode is used with {@link #seekTo(long, int)} to move media position to |
| * a sync (or key) frame associated with a media item that is located |
| * closest to (in time) or at the given time. |
| * |
| * @see #seekTo(long, int) |
| */ |
| public static final int SEEK_CLOSEST_SYNC = 0x02; |
| /** |
| * This mode is used with {@link #seekTo(long, int)} to move media position to |
| * a frame (not necessarily a key frame) associated with a media item that |
| * is located closest to or at the given time. |
| * |
| * @see #seekTo(long, int) |
| */ |
| public static final int SEEK_CLOSEST = 0x03; |
| |
| /** @hide */ |
| @IntDef(flag = false, /*prefix = "SEEK",*/ value = { |
| SEEK_PREVIOUS_SYNC, |
| SEEK_NEXT_SYNC, |
| SEEK_CLOSEST_SYNC, |
| SEEK_CLOSEST, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public @interface SeekMode {} |
| |
| /** |
| * The return value of {@link #getSelectedTrack} when there is no selected track for the given |
| * type. |
| * @see #getSelectedTrack(int) |
| */ |
| public static final int NO_TRACK_SELECTED = Integer.MIN_VALUE; |
| |
| static final PlaybackParams DEFAULT_PLAYBACK_PARAMS = new PlaybackParams.Builder() |
| .setSpeed(1f) |
| .setPitch(1f) |
| .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT) |
| .build(); |
| |
| private static final int CALL_COMPLETE_PLAYLIST_BASE = -1000; |
| private static final int END_OF_PLAYLIST = -1; |
| private static final int NO_MEDIA_ITEM = -2; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static ArrayMap<Integer, Integer> sResultCodeMap; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static ArrayMap<Integer, Integer> sErrorCodeMap; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static ArrayMap<Integer, Integer> sInfoCodeMap; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static ArrayMap<Integer, Integer> sSeekModeMap; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static ArrayMap<Integer, Integer> sPrepareDrmStatusMap; |
| |
| static { |
| sResultCodeMap = new ArrayMap<>(); |
| sResultCodeMap.put(MediaPlayer2.CALL_STATUS_NO_ERROR, RESULT_SUCCESS); |
| sResultCodeMap.put(MediaPlayer2.CALL_STATUS_ERROR_UNKNOWN, RESULT_ERROR_UNKNOWN); |
| sResultCodeMap.put( |
| MediaPlayer2.CALL_STATUS_INVALID_OPERATION, RESULT_ERROR_INVALID_STATE); |
| sResultCodeMap.put(MediaPlayer2.CALL_STATUS_BAD_VALUE, RESULT_ERROR_BAD_VALUE); |
| sResultCodeMap.put( |
| MediaPlayer2.CALL_STATUS_PERMISSION_DENIED, RESULT_ERROR_PERMISSION_DENIED); |
| sResultCodeMap.put(MediaPlayer2.CALL_STATUS_ERROR_IO, RESULT_ERROR_IO); |
| sResultCodeMap.put(MediaPlayer2.CALL_STATUS_SKIPPED, RESULT_INFO_SKIPPED); |
| |
| sErrorCodeMap = new ArrayMap<>(); |
| sErrorCodeMap.put(MediaPlayer2.MEDIA_ERROR_UNKNOWN, PLAYER_ERROR_UNKNOWN); |
| sErrorCodeMap.put(MediaPlayer2.MEDIA_ERROR_IO, PLAYER_ERROR_IO); |
| sErrorCodeMap.put(MediaPlayer2.MEDIA_ERROR_MALFORMED, PLAYER_ERROR_MALFORMED); |
| sErrorCodeMap.put(MediaPlayer2.MEDIA_ERROR_UNSUPPORTED, PLAYER_ERROR_UNSUPPORTED); |
| sErrorCodeMap.put(MediaPlayer2.MEDIA_ERROR_TIMED_OUT, PLAYER_ERROR_TIMED_OUT); |
| |
| sInfoCodeMap = new ArrayMap<>(); |
| sInfoCodeMap.put( |
| MediaPlayer2.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START); |
| sInfoCodeMap.put( |
| MediaPlayer2.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING); |
| sInfoCodeMap.put(MediaPlayer2.MEDIA_INFO_BUFFERING_UPDATE, MEDIA_INFO_BUFFERING_UPDATE); |
| sInfoCodeMap.put(MediaPlayer2.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING); |
| sInfoCodeMap.put(MediaPlayer2.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE); |
| sInfoCodeMap.put(MediaPlayer2.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE); |
| sInfoCodeMap.put(MediaPlayer2.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING); |
| sInfoCodeMap.put(MediaPlayer2.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING); |
| |
| sSeekModeMap = new ArrayMap<>(); |
| sSeekModeMap.put(SEEK_PREVIOUS_SYNC, MediaPlayer2.SEEK_PREVIOUS_SYNC); |
| sSeekModeMap.put(SEEK_NEXT_SYNC, MediaPlayer2.SEEK_NEXT_SYNC); |
| sSeekModeMap.put(SEEK_CLOSEST_SYNC, MediaPlayer2.SEEK_CLOSEST_SYNC); |
| sSeekModeMap.put(SEEK_CLOSEST, MediaPlayer2.SEEK_CLOSEST); |
| |
| sPrepareDrmStatusMap = new ArrayMap<>(); |
| sPrepareDrmStatusMap.put(MediaPlayer2.PREPARE_DRM_STATUS_SUCCESS, |
| DrmResult.RESULT_SUCCESS); |
| sPrepareDrmStatusMap.put(MediaPlayer2.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, |
| DrmResult.RESULT_ERROR_PROVISIONING_NETWORK_ERROR); |
| sPrepareDrmStatusMap.put(MediaPlayer2.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, |
| DrmResult.RESULT_ERROR_PREPARATION_ERROR); |
| sPrepareDrmStatusMap.put(MediaPlayer2.PREPARE_DRM_STATUS_PREPARATION_ERROR, |
| DrmResult.RESULT_ERROR_PREPARATION_ERROR); |
| sPrepareDrmStatusMap.put(MediaPlayer2.PREPARE_DRM_STATUS_UNSUPPORTED_SCHEME, |
| DrmResult.RESULT_ERROR_UNSUPPORTED_SCHEME); |
| sPrepareDrmStatusMap.put(MediaPlayer2.PREPARE_DRM_STATUS_RESOURCE_BUSY, |
| DrmResult.RESULT_ERROR_RESOURCE_BUSY); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| MediaPlayer2 mPlayer; |
| private ExecutorService mExecutor; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static final class PendingCommand { |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| @MediaPlayer2.CallCompleted final int mCallType; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final ResolvableFuture mFuture; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final TrackInfo mTrackInfo; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| PendingCommand(int callType, ResolvableFuture future) { |
| this(callType, future, null); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| PendingCommand(int callType, ResolvableFuture future, TrackInfo trackInfo) { |
| mCallType = callType; |
| mFuture = future; |
| mTrackInfo = trackInfo; |
| } |
| } |
| |
| /* A list for tracking the commands submitted to MediaPlayer2.*/ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| @GuardedBy("mPendingCommands") |
| final ArrayDeque<PendingCommand> mPendingCommands = new ArrayDeque<>(); |
| |
| /** |
| * PendingFuture is a future for the result of execution which will be executed later via |
| * the onExecute() method. |
| */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| abstract static class PendingFuture<V extends PlayerResult> |
| extends AbstractResolvableFuture<V> { |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final boolean mIsSeekTo; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| boolean mExecuteCalled = false; |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| List<ResolvableFuture<V>> mFutures; |
| |
| PendingFuture(Executor executor) { |
| this(executor, false); |
| } |
| |
| PendingFuture(Executor executor, boolean isSeekTo) { |
| mIsSeekTo = isSeekTo; |
| addListener(new Runnable() { |
| @Override |
| public void run() { |
| if (isCancelled() && mExecuteCalled) { |
| cancelFutures(); |
| } |
| } |
| }, executor); |
| } |
| |
| @Override |
| public boolean set(@Nullable V value) { |
| return super.set(value); |
| } |
| |
| @Override |
| public boolean setException(Throwable throwable) { |
| return super.setException(throwable); |
| } |
| |
| public boolean execute() { |
| if (!mExecuteCalled && !isCancelled()) { |
| mExecuteCalled = true; |
| mFutures = onExecute(); |
| } |
| if (!isCancelled() && !isDone()) { |
| setResultIfFinished(); |
| } |
| return isCancelled() || isDone(); |
| } |
| |
| private void setResultIfFinished() { |
| V result = null; |
| for (int i = 0; i < mFutures.size(); ++i) { |
| ResolvableFuture<V> future = mFutures.get(i); |
| if (!future.isDone() && !future.isCancelled()) { |
| return; |
| } |
| try { |
| result = future.get(); |
| int resultCode = result.getResultCode(); |
| if (resultCode != RESULT_SUCCESS && resultCode != RESULT_INFO_SKIPPED) { |
| cancelFutures(); |
| set(result); |
| return; |
| } |
| } catch (Exception e) { |
| cancelFutures(); |
| setException(e); |
| return; |
| } |
| } |
| try { |
| set(result); |
| } catch (Exception e) { |
| setException(e); |
| } |
| } |
| |
| abstract List<ResolvableFuture<V>> onExecute(); |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void cancelFutures() { |
| for (ResolvableFuture<V> future : mFutures) { |
| if (!future.isCancelled() && !future.isDone()) { |
| future.cancel(true); |
| } |
| } |
| } |
| } |
| |
| /* A list of pending operations within this MediaPlayer that will be executed sequentially. */ |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| @GuardedBy("mPendingFutures") |
| final ArrayDeque<PendingFuture<? super PlayerResult>> mPendingFutures = new ArrayDeque<>(); |
| |
| private final Object mStateLock = new Object(); |
| @GuardedBy("mStateLock") |
| private @PlayerState int mState; |
| @GuardedBy("mStateLock") |
| private Map<MediaItem, Integer> mMediaItemToBuffState = new HashMap<>(); |
| @GuardedBy("mStateLock") |
| private boolean mClosed; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final AudioFocusHandler mAudioFocusHandler; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final Object mPlaylistLock = new Object(); |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| MediaItemList mPlaylist = new MediaItemList(); |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ArrayList<MediaItem> mShuffledList = new ArrayList<>(); |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| MediaMetadata mPlaylistMetadata; |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mRepeatMode; |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mShuffleMode; |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mCurrentShuffleIdx; |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| MediaItem mCurPlaylistItem; |
| @GuardedBy("mPlaylistLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| MediaItem mNextPlaylistItem; |
| @GuardedBy("mPlaylistLock") |
| private boolean mSetMediaItemCalled; |
| |
| /** |
| * Constructor to create a MediaPlayer instance. |
| * |
| * @param context A {@link Context} that will be used to resolve {@link UriMediaItem}. |
| */ |
| public MediaPlayer(@NonNull Context context) { |
| if (context == null) { |
| throw new NullPointerException("context shouldn't be null"); |
| } |
| mState = PLAYER_STATE_IDLE; |
| mPlayer = MediaPlayer2.create(context); |
| mExecutor = Executors.newFixedThreadPool(1); |
| mPlayer.setEventCallback(mExecutor, new Mp2Callback()); |
| mPlayer.setDrmEventCallback(mExecutor, new Mp2DrmCallback()); |
| mCurrentShuffleIdx = NO_MEDIA_ITEM; |
| mAudioFocusHandler = new AudioFocusHandler(context, this); |
| } |
| |
| @GuardedBy("mPendingCommands") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void addPendingCommandLocked( |
| int callType, final ResolvableFuture future, final Object token) { |
| final PendingCommand pendingCommand = new PendingCommand(callType, future); |
| mPendingCommands.add(pendingCommand); |
| addFutureListener(pendingCommand, future, token); |
| } |
| |
| @GuardedBy("mPendingCommands") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void addPendingCommandWithTrackInfoLocked( |
| int callType, final ResolvableFuture future, final TrackInfo trackInfo, |
| final Object token) { |
| final PendingCommand pendingCommand = new PendingCommand(callType, future, trackInfo); |
| mPendingCommands.add(pendingCommand); |
| addFutureListener(pendingCommand, future, token); |
| } |
| |
| @GuardedBy("mPendingCommands") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void addFutureListener(final PendingCommand pendingCommand, final ResolvableFuture future, |
| final Object token) { |
| future.addListener(new Runnable() { |
| @Override |
| public void run() { |
| // Propagate the cancellation to the MediaPlayer2 implementation. |
| if (future.isCancelled()) { |
| synchronized (mPendingCommands) { |
| if (mPlayer.cancel(token)) { |
| mPendingCommands.remove(pendingCommand); |
| } |
| } |
| } |
| } |
| }, mExecutor); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private void addPendingFuture(final PendingFuture pendingFuture) { |
| synchronized (mPendingFutures) { |
| mPendingFutures.add(pendingFuture); |
| executePendingFutures(); |
| } |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> play() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| final ResolvableFuture<PlayerResult> future; |
| if (mAudioFocusHandler.onPlay()) { |
| if (mPlayer.getAudioAttributes() == null) { |
| futures.add(setPlayerVolumeInternal(0f)); |
| } |
| future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.play(); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PLAY, future, token); |
| } |
| } else { |
| future = createFutureForResultCode(RESULT_ERROR_UNKNOWN); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> pause() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| mAudioFocusHandler.onPause(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.pause(); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PAUSE, future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Prepares the media items for playback. |
| * <p> |
| * After setting the media items and the display surface, you need to call this method. |
| * During this preparation, the player may allocate resources required to play, such as audio |
| * and video decoders. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> prepare() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.prepare(); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PREPARE, future, token); |
| } |
| // TODO: Changing buffering state is not correct. Think about changing MP2 event |
| // APIs for the initial buffering for prepare case. |
| setBufferingState(mPlayer.getCurrentMediaItem(), |
| BUFFERING_STATE_BUFFERING_AND_STARVED); |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> seekTo(final long position) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = |
| new PendingFuture<PlayerResult>(mExecutor, true) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.seekTo(position); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SEEK_TO, future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Sets the playback speed. {@code 1.0f} is the default, and values less than or equal to |
| * {@code 0.0f} are not allowed. |
| * <p> |
| * The supported playback speed range depends on the underlying player implementation, so it is |
| * recommended to query the actual speed of the player via {@link #getPlaybackSpeed()} after the |
| * operation completes. |
| * |
| * @param playbackSpeed The requested playback speed. |
| * @return A {@link ListenableFuture} representing the pending completion of the command. |
| */ |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> setPlaybackSpeed( |
| @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false) |
| final float playbackSpeed) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| if (playbackSpeed <= 0.0f) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE); |
| } |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setPlaybackParams(new PlaybackParams.Builder( |
| mPlayer.getPlaybackParams()) |
| .setSpeed(playbackSpeed).build()); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS, |
| future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> setAudioAttributes( |
| @NonNull final AudioAttributesCompat attr) { |
| if (attr == null) { |
| throw new NullPointerException("attr shouldn't be null"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setAudioAttributes(attr); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, |
| future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @PlayerState |
| public int getPlayerState() { |
| synchronized (mStateLock) { |
| return mState; |
| } |
| } |
| |
| @Override |
| public long getCurrentPosition() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return UNKNOWN_TIME; |
| } |
| } |
| try { |
| final long pos = mPlayer.getCurrentPosition(); |
| if (pos >= 0) { |
| return pos; |
| } |
| } catch (IllegalStateException e) { |
| // fall-through. |
| } |
| return UNKNOWN_TIME; |
| } |
| |
| @Override |
| public long getDuration() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return UNKNOWN_TIME; |
| } |
| } |
| try { |
| final long duration = mPlayer.getDuration(); |
| if (duration >= 0) { |
| return duration; |
| } |
| } catch (IllegalStateException e) { |
| // fall-through. |
| } |
| return UNKNOWN_TIME; |
| } |
| |
| @Override |
| public long getBufferedPosition() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return UNKNOWN_TIME; |
| } |
| } |
| try { |
| final long pos = mPlayer.getBufferedPosition(); |
| if (pos >= 0) { |
| return pos; |
| } |
| } catch (IllegalStateException e) { |
| // fall-through. |
| } |
| return UNKNOWN_TIME; |
| } |
| |
| @Override |
| @BuffState |
| public int getBufferingState() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return BUFFERING_STATE_UNKNOWN; |
| } |
| } |
| Integer buffState; |
| synchronized (mStateLock) { |
| buffState = mMediaItemToBuffState.get(mPlayer.getCurrentMediaItem()); |
| } |
| return buffState == null ? BUFFERING_STATE_UNKNOWN : buffState; |
| } |
| |
| @Override |
| @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false) |
| public float getPlaybackSpeed() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return 1.0f; |
| } |
| } |
| try { |
| return mPlayer.getPlaybackParams().getSpeed(); |
| } catch (IllegalStateException e) { |
| return 1.0f; |
| } |
| } |
| |
| @Override |
| @Nullable |
| public AudioAttributesCompat getAudioAttributes() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return null; |
| } |
| } |
| try { |
| return mPlayer.getAudioAttributes(); |
| } catch (IllegalStateException e) { |
| return null; |
| } |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> setMediaItem(@NonNull final MediaItem item) { |
| if (item == null) { |
| throw new NullPointerException("item shouldn't be null"); |
| } |
| if (item instanceof FileMediaItem) { |
| if (((FileMediaItem) item).isClosed()) { |
| throw new IllegalArgumentException("File descriptor is closed. " + item); |
| } |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| synchronized (mPlaylistLock) { |
| mPlaylist.clear(); |
| mShuffledList.clear(); |
| mCurPlaylistItem = item; |
| mNextPlaylistItem = null; |
| mCurrentShuffleIdx = END_OF_PLAYLIST; |
| } |
| futures.addAll(setMediaItemsInternal(item, null)); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> setPlaylist( |
| @NonNull final List<MediaItem> playlist, @Nullable final MediaMetadata metadata) { |
| if (playlist == null) { |
| throw new NullPointerException("playlist shouldn't be null"); |
| } else if (playlist.isEmpty()) { |
| throw new IllegalArgumentException("playlist shouldn't be empty"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| String errorString = null; |
| for (MediaItem item : playlist) { |
| if (item == null) { |
| errorString = "playlist shouldn't contain null item"; |
| break; |
| } |
| if (item instanceof FileMediaItem) { |
| if (((FileMediaItem) item).isClosed()) { |
| errorString = "File descriptor is closed. " + item; |
| break; |
| } |
| } |
| } |
| if (errorString != null) { |
| // Close all the given FileMediaItems on error case. |
| for (MediaItem item : playlist) { |
| if (item instanceof FileMediaItem) { |
| ((FileMediaItem) item).increaseRefCount(); |
| ((FileMediaItem) item).decreaseRefCount(); |
| } |
| } |
| throw new IllegalArgumentException(errorString); |
| } |
| |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| MediaItem curItem; |
| MediaItem nextItem; |
| synchronized (mPlaylistLock) { |
| mPlaylistMetadata = metadata; |
| mPlaylist.replaceAll(playlist); |
| applyShuffleModeLocked(); |
| mCurrentShuffleIdx = 0; |
| updateAndGetCurrentNextItemIfNeededLocked(); |
| curItem = mCurPlaylistItem; |
| nextItem = mNextPlaylistItem; |
| } |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata); |
| } |
| }); |
| if (curItem != null) { |
| return setMediaItemsInternal(curItem, nextItem); |
| } |
| return createFuturesForResultCode(RESULT_SUCCESS); |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> addPlaylistItem( |
| final int index, @NonNull final MediaItem item) { |
| if (item == null) { |
| throw new NullPointerException("item shouldn't be null"); |
| } |
| if (item instanceof FileMediaItem) { |
| if (((FileMediaItem) item).isClosed()) { |
| throw new IllegalArgumentException("File descriptor is closed. " + item); |
| } |
| } |
| if (index < 0) { |
| throw new IllegalArgumentException("index shouldn't be negative"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| Pair<MediaItem, MediaItem> updatedCurNextItem; |
| synchronized (mPlaylistLock) { |
| if (mPlaylist.contains(item)) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE, item); |
| } |
| int clampedIndex = clamp(index, mPlaylist.size()); |
| int addedShuffleIdx = clampedIndex; |
| mPlaylist.add(clampedIndex, item); |
| if (mShuffleMode == SessionPlayer.SHUFFLE_MODE_NONE) { |
| mShuffledList.add(clampedIndex, item); |
| } else { |
| // Add the item in random position of mShuffledList. |
| addedShuffleIdx = (int) (Math.random() * (mShuffledList.size() + 1)); |
| mShuffledList.add(addedShuffleIdx, item); |
| } |
| if (addedShuffleIdx <= mCurrentShuffleIdx) { |
| mCurrentShuffleIdx++; |
| } |
| updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked(); |
| } |
| final List<MediaItem> playlist = getPlaylist(); |
| final MediaMetadata metadata = getPlaylistMetadata(); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata); |
| } |
| }); |
| |
| if (updatedCurNextItem.second == null) { |
| return createFuturesForResultCode(RESULT_SUCCESS); |
| } |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| futures.add(setNextMediaItemInternal(updatedCurNextItem.second)); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> removePlaylistItem(@IntRange(from = 0) final int index) { |
| if (index < 0) { |
| throw new IllegalArgumentException("index shouldn't be negative"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| int removedItemShuffleIdx; |
| MediaItem curItem; |
| MediaItem nextItem; |
| Pair<MediaItem, MediaItem> updatedCurNextItem = null; |
| synchronized (mPlaylistLock) { |
| if (index >= mPlaylist.size()) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE); |
| } |
| MediaItem item = mPlaylist.remove(index); |
| removedItemShuffleIdx = mShuffledList.indexOf(item); |
| mShuffledList.remove(removedItemShuffleIdx); |
| if (removedItemShuffleIdx < mCurrentShuffleIdx) { |
| mCurrentShuffleIdx--; |
| } |
| updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked(); |
| curItem = mCurPlaylistItem; |
| nextItem = mNextPlaylistItem; |
| } |
| final List<MediaItem> playlist = getPlaylist(); |
| final MediaMetadata metadata = getPlaylistMetadata(); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata); |
| } |
| }); |
| |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| if (updatedCurNextItem != null) { |
| if (updatedCurNextItem.first != null) { |
| futures.addAll(setMediaItemsInternal(curItem, nextItem)); |
| } else if (updatedCurNextItem.second != null) { |
| futures.add(setNextMediaItemInternal(nextItem)); |
| } |
| } else { |
| futures.add(createFutureForResultCode(RESULT_SUCCESS)); |
| } |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> replacePlaylistItem( |
| final int index, @NonNull final MediaItem item) { |
| if (item == null) { |
| throw new NullPointerException("item shouldn't be null"); |
| } |
| if (item instanceof FileMediaItem) { |
| if (((FileMediaItem) item).isClosed()) { |
| throw new IllegalArgumentException("File descriptor is closed. " + item); |
| } |
| } |
| if (index < 0) { |
| throw new IllegalArgumentException("index shouldn't be negative"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| MediaItem curItem; |
| MediaItem nextItem; |
| Pair<MediaItem, MediaItem> updatedCurNextItem = null; |
| synchronized (mPlaylistLock) { |
| if (index >= mPlaylist.size() || mPlaylist.contains(item)) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE, item); |
| } |
| |
| int shuffleIdx = mShuffledList.indexOf(mPlaylist.get(index)); |
| mShuffledList.set(shuffleIdx, item); |
| mPlaylist.set(index, item); |
| updatedCurNextItem = updateAndGetCurrentNextItemIfNeededLocked(); |
| curItem = mCurPlaylistItem; |
| nextItem = mNextPlaylistItem; |
| } |
| // TODO: Should we notify current media item changed if it is replaced? |
| final List<MediaItem> playlist = getPlaylist(); |
| final MediaMetadata metadata = getPlaylistMetadata(); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onPlaylistChanged(MediaPlayer.this, playlist, metadata); |
| } |
| }); |
| |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| if (updatedCurNextItem != null) { |
| if (updatedCurNextItem.first != null) { |
| futures.addAll(setMediaItemsInternal(curItem, nextItem)); |
| } else if (updatedCurNextItem.second != null) { |
| futures.add(setNextMediaItemInternal(nextItem)); |
| } |
| } else { |
| futures.add(createFutureForResultCode(RESULT_SUCCESS)); |
| } |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| MediaItem curItem; |
| MediaItem nextItem; |
| synchronized (mPlaylistLock) { |
| if (mCurrentShuffleIdx < 0) { |
| return createFuturesForResultCode(RESULT_ERROR_INVALID_STATE); |
| } |
| int prevShuffleIdx = mCurrentShuffleIdx - 1; |
| if (prevShuffleIdx < 0) { |
| if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) { |
| prevShuffleIdx = mShuffledList.size() - 1; |
| } else { |
| return createFuturesForResultCode(RESULT_ERROR_INVALID_STATE); |
| } |
| } |
| mCurrentShuffleIdx = prevShuffleIdx; |
| updateAndGetCurrentNextItemIfNeededLocked(); |
| curItem = mCurPlaylistItem; |
| nextItem = mNextPlaylistItem; |
| } |
| return setMediaItemsInternal(curItem, nextItem); |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> skipToNextPlaylistItem() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| MediaItem curItem; |
| MediaItem nextItem; |
| synchronized (mPlaylistLock) { |
| if (mCurrentShuffleIdx < 0) { |
| return createFuturesForResultCode(RESULT_ERROR_INVALID_STATE); |
| } |
| int nextShuffleIdx = mCurrentShuffleIdx + 1; |
| if (nextShuffleIdx >= mShuffledList.size()) { |
| if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) { |
| nextShuffleIdx = 0; |
| } else { |
| return createFuturesForResultCode(RESULT_ERROR_INVALID_STATE); |
| } |
| } |
| mCurrentShuffleIdx = nextShuffleIdx; |
| updateAndGetCurrentNextItemIfNeededLocked(); |
| curItem = mCurPlaylistItem; |
| nextItem = mNextPlaylistItem; |
| } |
| if (curItem != null) { |
| return setMediaItemsInternal(curItem, nextItem); |
| } |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| futures.add(skipToNextInternal()); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> skipToPlaylistItem(@IntRange(from = 0) final int index) { |
| if (index < 0) { |
| throw new IllegalArgumentException("index shouldn't be negative"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| MediaItem curItem; |
| MediaItem nextItem; |
| synchronized (mPlaylistLock) { |
| if (index >= mPlaylist.size()) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE); |
| } |
| mCurrentShuffleIdx = mShuffledList.indexOf(mPlaylist.get(index)); |
| updateAndGetCurrentNextItemIfNeededLocked(); |
| curItem = mCurPlaylistItem; |
| nextItem = mNextPlaylistItem; |
| } |
| return setMediaItemsInternal(curItem, nextItem); |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> updatePlaylistMetadata( |
| @Nullable final MediaMetadata metadata) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| synchronized (mPlaylistLock) { |
| mPlaylistMetadata = metadata; |
| } |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onPlaylistMetadataChanged(MediaPlayer.this, metadata); |
| } |
| }); |
| return createFuturesForResultCode(RESULT_SUCCESS); |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> setRepeatMode(final int repeatMode) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| if (repeatMode < SessionPlayer.REPEAT_MODE_NONE |
| || repeatMode > SessionPlayer.REPEAT_MODE_GROUP) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE); |
| } |
| |
| boolean changed; |
| synchronized (mPlaylistLock) { |
| changed = mRepeatMode != repeatMode; |
| mRepeatMode = repeatMode; |
| } |
| if (changed) { |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onRepeatModeChanged(MediaPlayer.this, repeatMode); |
| } |
| }); |
| } |
| return createFuturesForResultCode(RESULT_SUCCESS); |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> setShuffleMode(final int shuffleMode) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| if (shuffleMode < SessionPlayer.SHUFFLE_MODE_NONE |
| || shuffleMode > SessionPlayer.SHUFFLE_MODE_GROUP) { |
| return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE); |
| } |
| |
| boolean changed; |
| synchronized (mPlaylistLock) { |
| changed = mShuffleMode != shuffleMode; |
| mShuffleMode = shuffleMode; |
| } |
| if (changed) { |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onShuffleModeChanged(MediaPlayer.this, shuffleMode); |
| } |
| }); |
| } |
| return createFuturesForResultCode(RESULT_SUCCESS); |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| @Override |
| @Nullable |
| public List<MediaItem> getPlaylist() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return null; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| return mPlaylist.isEmpty() ? null : new ArrayList<>(mPlaylist.getCollection()); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public MediaMetadata getPlaylistMetadata() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return null; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| return mPlaylistMetadata; |
| } |
| } |
| |
| @Override |
| public int getRepeatMode() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return REPEAT_MODE_NONE; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| return mRepeatMode; |
| } |
| } |
| |
| @Override |
| public int getShuffleMode() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return SHUFFLE_MODE_NONE; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| return mShuffleMode; |
| } |
| } |
| |
| @Override |
| @Nullable |
| public MediaItem getCurrentMediaItem() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return null; |
| } |
| } |
| return mPlayer.getCurrentMediaItem(); |
| } |
| |
| @Override |
| public int getCurrentMediaItemIndex() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return END_OF_PLAYLIST; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| if (mCurrentShuffleIdx < 0) { |
| return END_OF_PLAYLIST; |
| } |
| return mPlaylist.indexOf(mShuffledList.get(mCurrentShuffleIdx)); |
| } |
| } |
| |
| @Override |
| public int getPreviousMediaItemIndex() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return END_OF_PLAYLIST; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| if (mCurrentShuffleIdx < 0) { |
| return END_OF_PLAYLIST; |
| } |
| int prevShuffleIdx = mCurrentShuffleIdx - 1; |
| if (prevShuffleIdx < 0) { |
| if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) { |
| return mPlaylist.indexOf(mShuffledList.get(mShuffledList.size() - 1)); |
| } else { |
| return END_OF_PLAYLIST; |
| } |
| } |
| return mPlaylist.indexOf(mShuffledList.get(prevShuffleIdx)); |
| } |
| } |
| |
| @Override |
| public int getNextMediaItemIndex() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return END_OF_PLAYLIST; |
| } |
| } |
| synchronized (mPlaylistLock) { |
| if (mCurrentShuffleIdx < 0) { |
| return END_OF_PLAYLIST; |
| } |
| int nextShuffleIdx = mCurrentShuffleIdx + 1; |
| if (nextShuffleIdx >= mShuffledList.size()) { |
| if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) { |
| return mPlaylist.indexOf(mShuffledList.get(0)); |
| } else { |
| return END_OF_PLAYLIST; |
| } |
| } |
| return mPlaylist.indexOf(mShuffledList.get(nextShuffleIdx)); |
| } |
| } |
| |
| @Override |
| public void close() throws Exception { |
| synchronized (mStateLock) { |
| if (!mClosed) { |
| mClosed = true; |
| reset(); |
| mAudioFocusHandler.close(); |
| mPlayer.close(); |
| mExecutor.shutdown(); |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) |
| public AudioFocusHandler getAudioFocusHandler() { |
| return mAudioFocusHandler; |
| } |
| |
| /** |
| * Resets {@link MediaPlayer} to its uninitialized state if not closed. After calling |
| * this method, you will have to initialize it again by setting the media item and |
| * calling {@link #prepare()}. |
| * <p> Note that if the player is closed, there is no way to reuse the instance. |
| */ |
| public void reset() { |
| // Cancel the pending commands. |
| synchronized (mPendingCommands) { |
| for (PendingCommand c : mPendingCommands) { |
| c.mFuture.cancel(true); |
| } |
| mPendingCommands.clear(); |
| } |
| // Cancel the pending futures. |
| synchronized (mPendingFutures) { |
| for (PendingFuture f : mPendingFutures) { |
| if (f.mExecuteCalled && !f.isDone() && !f.isCancelled()) { |
| f.cancel(true); |
| } |
| } |
| mPendingFutures.clear(); |
| } |
| synchronized (mStateLock) { |
| mState = PLAYER_STATE_IDLE; |
| mMediaItemToBuffState.clear(); |
| } |
| synchronized (mPlaylistLock) { |
| mPlaylist.clear(); |
| mShuffledList.clear(); |
| mCurPlaylistItem = null; |
| mNextPlaylistItem = null; |
| mCurrentShuffleIdx = END_OF_PLAYLIST; |
| mSetMediaItemCalled = false; |
| } |
| mAudioFocusHandler.onReset(); |
| mPlayer.reset(); |
| } |
| |
| /** |
| * Sets the {@link Surface} to be used as the sink for the video portion of |
| * the media. |
| * <p> |
| * A null surface will result in only the audio track being played. |
| * <p> |
| * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps |
| * returned from {@link SurfaceTexture#getTimestamp()} will have an |
| * unspecified zero point. These timestamps cannot be directly compared |
| * between different media sources, different instances of the same media |
| * source, or multiple runs of the same program. The timestamp is normally |
| * monotonically increasing and is unaffected by time-of-day adjustments, |
| * but it is reset when the position is set. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @param surface The {@link Surface} to be used for the video portion of |
| * the media. |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> setSurface(@Nullable final Surface surface) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setSurface(surface); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_SURFACE, future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| @Override |
| @NonNull |
| public ListenableFuture<PlayerResult> setSurfaceInternal(@Nullable Surface surface) { |
| return setSurface(surface); |
| } |
| |
| /** |
| * Sets the volume of the audio of the media to play, expressed as a linear multiplier |
| * on the audio samples. |
| * <p> |
| * Note that this volume is specific to the player, and is separate from stream volume |
| * used across the platform. |
| * <p> |
| * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified |
| * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player. |
| * <p> |
| * The default player volume is 1.0f. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}. |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> setPlayerVolume( |
| @FloatRange(from = 0, to = 1) final float volume) { |
| if (volume < 0 || volume > 1) { |
| throw new IllegalArgumentException("volume should be between 0.0 and 1.0"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| futures.add(setPlayerVolumeInternal(volume)); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * @return the current volume of this player to this player. Note that it does not take into |
| * account the associated stream volume. |
| */ |
| public float getPlayerVolume() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return 1.0f; |
| } |
| } |
| return mPlayer.getPlayerVolume(); |
| } |
| |
| /** |
| * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}. |
| */ |
| public float getMaxPlayerVolume() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return 1.0f; |
| } |
| } |
| return mPlayer.getMaxPlayerVolume(); |
| } |
| |
| |
| /** |
| * Returns the size of the video. |
| * |
| * @return the size of the video. The width and height of size could be 0 if there is no video |
| * or the size has not been determined yet. |
| * The {@link PlayerCallback} can be registered via {@link #registerPlayerCallback} to |
| * receive a notification {@link PlayerCallback#onVideoSizeChanged} when the size |
| * is available. |
| */ |
| @NonNull |
| public VideoSize getVideoSize() { |
| androidx.media2.common.VideoSize sizeInternal = getVideoSizeInternal(); |
| return new VideoSize(sizeInternal); |
| } |
| |
| /** @hide */ |
| @RestrictTo(LIBRARY_GROUP) |
| @Override |
| @NonNull |
| public androidx.media2.common.VideoSize getVideoSizeInternal() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return new androidx.media2.common.VideoSize(0, 0); |
| } |
| } |
| return new androidx.media2.common.VideoSize(mPlayer.getVideoWidth(), |
| mPlayer.getVideoHeight()); |
| } |
| |
| /** |
| * @return a {@link PersistableBundle} containing the set of attributes and values |
| * available for the media being handled by this player instance. |
| * The attributes are described in {@link MetricsConstants}. |
| * |
| * Additional vendor-specific fields may also be present in the return value. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| @RequiresApi(21) |
| public PersistableBundle getMetrics() { |
| return mPlayer.getMetrics(); |
| } |
| |
| /** |
| * Sets playback params using {@link PlaybackParams}. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @param params the playback params. |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> setPlaybackParams(@NonNull final PlaybackParams params) { |
| if (params == null) { |
| throw new NullPointerException("params shouldn't be null"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setPlaybackParams(params); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS, |
| future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Gets the playback params, containing the current playback rate. |
| * |
| * @return the playback params. |
| */ |
| @NonNull |
| public PlaybackParams getPlaybackParams() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return DEFAULT_PLAYBACK_PARAMS; |
| } |
| } |
| return mPlayer.getPlaybackParams(); |
| } |
| |
| /** |
| * Moves the media to specified time position by considering the given mode. |
| * <p> |
| * There is at most one active seekTo processed at any time. If there is a to-be-completed |
| * seekTo, new seekTo requests will be queued in such a way that only the last request |
| * is kept. When current seekTo is completed, the queued request will be processed if |
| * that request is different from just-finished seekTo operation, i.e., the requested |
| * position or mode is different. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @param position the offset in milliseconds from the start to seek to. |
| * When seeking to the given time position, there is no guarantee that the media item |
| * has a frame located at the position. When this happens, a frame nearby will be rendered. |
| * The value should be in the range of start and end positions defined in {@link MediaItem}. |
| * @param mode the mode indicating where exactly to seek to. |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> seekTo(final long position, @SeekMode final int mode) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = |
| new PendingFuture<PlayerResult>(mExecutor, true) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| int mp2SeekMode = sSeekModeMap.containsKey(mode) |
| ? sSeekModeMap.get(mode) : MediaPlayer2.SEEK_NEXT_SYNC; |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.seekTo(position, mp2SeekMode); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SEEK_TO, future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Gets current playback position as a {@link MediaTimestamp}. |
| * <p> |
| * The MediaTimestamp represents how the media time correlates to the system time in |
| * a linear fashion using an anchor and a clock rate. During regular playback, the media |
| * time moves fairly constantly (though the anchor frame may be rebased to a current |
| * system time, the linear correlation stays steady). Therefore, this method does not |
| * need to be called often. |
| * <p> |
| * To help users get current playback position, this method always anchors the timestamp |
| * to the current {@link System#nanoTime system time}, so |
| * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position. |
| * |
| * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp |
| * is available, e.g. because the media player has not been initialized. |
| * |
| * @see MediaTimestamp |
| */ |
| @Nullable |
| public MediaTimestamp getTimestamp() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return null; |
| } |
| } |
| return mPlayer.getTimestamp(); |
| } |
| |
| /** |
| * Sets the audio session ID. |
| * |
| * @param sessionId the audio session ID. |
| * The audio session ID is a system wide unique identifier for the audio stream played by |
| * this MediaPlayer2 instance. |
| * The primary use of the audio session ID is to associate audio effects to a particular |
| * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect, |
| * this effect will be applied only to the audio content of media players within the same |
| * audio session and not to the output mix. |
| * When created, a MediaPlayer2 instance automatically generates its own audio session ID. |
| * However, it is possible to force this player to be part of an already existing audio session |
| * by calling this method. |
| * <p>This method must be called before {@link #setMediaItem} and {@link #setPlaylist} methods. |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @see AudioManager#generateAudioSessionId |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> setAudioSessionId(final int sessionId) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setAudioSessionId(sessionId); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_AUDIO_SESSION_ID, |
| future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Returns the audio session ID. |
| * |
| * @return the audio session ID. {@see #setAudioSessionId(int)} |
| * Note that the audio session ID is 0 if a problem occurred when the MediaPlayer was |
| * constructed or it is closed. |
| */ |
| public int getAudioSessionId() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return 0; |
| } |
| } |
| return mPlayer.getAudioSessionId(); |
| } |
| |
| /** |
| * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation |
| * effect which can be applied on any sound source that directs a certain amount of its |
| * energy to this effect. This amount is defined by setAuxEffectSendLevel(). |
| * See {@link #setAuxEffectSendLevel(float)}. |
| * <p>After creating an auxiliary effect (e.g. |
| * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with |
| * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method |
| * to attach the player to the effect. |
| * <p>To detach the effect from the player, call this method with a null effect id. |
| * <p>This method must be called before {@link #setMediaItem} and {@link #setPlaylist} methods. |
| * @param effectId system wide unique id of the effect to attach |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> attachAuxEffect(final int effectId) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.attachAuxEffect(effectId); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_ATTACH_AUX_EFFECT, |
| future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| |
| /** |
| * Sets the send level of the player to the attached auxiliary effect. |
| * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0. |
| * <p>By default the send level is 0, so even if an effect is attached to the player |
| * this method must be called for the effect to be applied. |
| * <p>Note that the passed level value is a raw scalar. UI controls should be scaled |
| * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB, |
| * so an appropriate conversion from linear UI input x to level is: |
| * x == 0 -> level = 0 |
| * 0 < x <= R -> level = 10^(72*(x-R)/20/R) |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @param level send level scalar |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| @NonNull |
| public ListenableFuture<PlayerResult> setAuxEffectSendLevel( |
| @FloatRange(from = 0, to = 1) final float level) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setAuxEffectSendLevel(level); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, |
| future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Returns a List of track information. |
| * |
| * @return List of track info. The total number of tracks is the size of the list. |
| */ |
| @NonNull |
| public List<TrackInfo> getTrackInfo() { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return Collections.emptyList(); |
| } |
| } |
| List<MediaPlayer2.TrackInfo> info2s = mPlayer.getTrackInfo(); |
| MediaItem item = mPlayer.getCurrentMediaItem(); |
| List<TrackInfo> infos = new ArrayList<>(); |
| for (int index = 0; index < info2s.size(); index++) { |
| MediaPlayer2.TrackInfo info2 = info2s.get(index); |
| infos.add(new TrackInfo(index, item, info2.getTrackType(), info2.getFormat())); |
| } |
| return infos; |
| } |
| |
| @NonNull |
| TrackInfo getTrackInfo(int index) { |
| List<MediaPlayer2.TrackInfo> info2s = mPlayer.getTrackInfo(); |
| MediaPlayer2.TrackInfo info2 = info2s.get(index); |
| MediaItem item = mPlayer.getCurrentMediaItem(); |
| return new TrackInfo(index, item, info2.getTrackType(), info2.getFormat()); |
| } |
| |
| /** |
| * Returns the audio or video track currently selected for playback. |
| * The return value is an element in the list returned by {@link #getTrackInfo()}, and can |
| * be used in calls to {@link #selectTrack(TrackInfo)}. |
| * |
| * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO} or |
| * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO} |
| * @return metadata corresponding to the audio or video track currently selected for |
| * playback; {@code null} is returned when there is no selected track for {@code trackType} or |
| * when {@code trackType} is not one of audio or video. |
| * @throws IllegalStateException if called after {@link #close()} |
| * |
| * @see #getTrackInfo() |
| * @see #selectTrack(TrackInfo) |
| */ |
| // TODO: revise the method document once subtitle track support is re-enabled. (b/130312596) |
| @Nullable |
| public TrackInfo getSelectedTrack(@TrackInfo.MediaTrackType int trackType) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return null; |
| } |
| } |
| final int ret = mPlayer.getSelectedTrack(trackType); |
| return ret < 0 ? null : getTrackInfo(ret); |
| } |
| |
| /** |
| * Selects a track. |
| * <p> |
| * If the player is in invalid state, |
| * {@link SessionPlayer.PlayerResult#RESULT_ERROR_INVALID_STATE} will be |
| * reported with {@link SessionPlayer.PlayerResult}. |
| * If a player is in <em>Playing</em> state, the selected track is presented immediately. |
| * If a player is not in Playing state, it just marks the track to be played. |
| * </p> |
| * <p> |
| * In any valid state, if it is called multiple times on the same type of track (ie. Video, |
| * Audio), the most recent one will be chosen. |
| * </p> |
| * <p> |
| * The first audio and video tracks are selected by default if available, even though |
| * this method is not called. |
| * </p> |
| * <p> |
| * Currently, audio tracks can be selected via this method. |
| * </p> |
| * @param trackInfo metadata corresponding to the track to be selected. A {@code trackInfo} |
| * object can be obtained from {@link #getTrackInfo()}. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @see #getTrackInfo |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| */ |
| // TODO: support subtitle track selection (b/130312596) |
| @NonNull |
| public ListenableFuture<PlayerResult> selectTrack(@NonNull final TrackInfo trackInfo) { |
| if (trackInfo == null) { |
| throw new NullPointerException("trackInfo shouldn't be null"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| final int trackId = trackInfo.getId(); |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| // TODO (b/131873726): trackId may be invalid |
| Object token = mPlayer.selectTrack(trackId); |
| addPendingCommandWithTrackInfoLocked(MediaPlayer2.CALL_COMPLETED_SELECT_TRACK, |
| future, trackInfo, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Deselects a track. |
| * <p> |
| * Currently, the track must be a subtitle track and no audio or video tracks can be |
| * deselected. |
| * </p> |
| * @param trackInfo metadata corresponding to the track to be selected. A {@code trackInfo} |
| * object can be obtained from {@link #getTrackInfo()}. |
| * <p> |
| * On success, a {@link SessionPlayer.PlayerResult} is returned with |
| * the current media item when the command completed. |
| * |
| * @see #getTrackInfo |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link SessionPlayer.PlayerResult} will be delivered when the command |
| * completed. |
| * |
| * @hide TODO: unhide this when we support subtitle track selection (b/130312596) |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| @NonNull |
| public ListenableFuture<PlayerResult> deselectTrack(@NonNull final TrackInfo trackInfo) { |
| if (trackInfo == null) { |
| throw new NullPointerException("trackInfo shouldn't be null"); |
| } |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return createFutureForClosed(); |
| } |
| } |
| final int trackId = trackInfo.getId(); |
| PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<PlayerResult>> onExecute() { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| // TODO (b/131873726): trackId may be invalid |
| Object token = mPlayer.deselectTrack(trackId); |
| addPendingCommandWithTrackInfoLocked(MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK, |
| future, trackInfo, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * TODO: Merge this into {@link #getTrackInfo()} (b/132928418) |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| @NonNull |
| @Override |
| public List<SessionPlayer.TrackInfo> getTrackInfoInternal() { |
| List<TrackInfo> list = getTrackInfo(); |
| List<SessionPlayer.TrackInfo> trackList = new ArrayList<>(); |
| for (int i = 0; i < list.size(); i++) { |
| trackList.add(createTrackInfoInternal(list.get(i))); |
| } |
| return trackList; |
| } |
| |
| /** |
| * TODO: Merge this into {@link #selectTrack(TrackInfo)} (b/132928418) |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> selectTrackInternal(SessionPlayer.TrackInfo info) { |
| return selectTrack(createTrackInfo(info)); |
| } |
| |
| /** |
| * TODO: Merge this into {@link #deselectTrack(TrackInfo)} (b/132928418) |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| @NonNull |
| @Override |
| public ListenableFuture<PlayerResult> deselectTrackInternal(SessionPlayer.TrackInfo info) { |
| return deselectTrack(createTrackInfo(info)); |
| } |
| |
| /** |
| * TODO: Merge this into {@link #getSelectedTrack(int)} (b/132928418) |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| @Nullable |
| @Override |
| public SessionPlayer.TrackInfo getSelectedTrackInternal(int trackType) { |
| return createTrackInfoInternal(getSelectedTrack(trackType)); |
| } |
| |
| /** |
| * Register {@link PlayerCallback} to listen changes. |
| * |
| * @param executor a callback Executor |
| * @param callback a PlayerCallback |
| * @throws IllegalArgumentException if executor or callback is {@code null}. |
| */ |
| public void registerPlayerCallback( |
| @NonNull /*@CallbackExecutor*/ Executor executor, |
| @NonNull PlayerCallback callback) { |
| super.registerPlayerCallback(executor, callback); |
| } |
| |
| /** |
| * Unregister the previously registered {@link PlayerCallback}. |
| * |
| * @param callback the callback to be removed |
| * @throws IllegalArgumentException if the callback is {@code null}. |
| */ |
| public void unregisterPlayerCallback(@NonNull PlayerCallback callback) { |
| super.unregisterPlayerCallback(callback); |
| } |
| |
| /** |
| * Retrieves the DRM Info associated with the current media item. |
| * |
| * @throws IllegalStateException if called before being prepared |
| * @hide |
| */ |
| @Nullable |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public DrmInfo getDrmInfo() { |
| MediaPlayer2.DrmInfo info = mPlayer.getDrmInfo(); |
| return info == null ? null : new DrmInfo(info); |
| } |
| |
| /** |
| * Prepares the DRM for the current media item. |
| * <p> |
| * If {@link OnDrmConfigHelper} is registered, it will be called during |
| * preparation to allow configuration of the DRM properties before opening the |
| * DRM session. Note that the callback is called synchronously in the thread that called |
| * {@link #prepareDrm}. It should be used only for a series of {@code getDrmPropertyString} |
| * and {@code setDrmPropertyString} calls and refrain from any lengthy operation. |
| * <p> |
| * If the device has not been provisioned before, this call also provisions the device |
| * which involves accessing the provisioning server and can take a variable time to |
| * complete depending on the network connectivity. |
| * prepareDrm() runs in non-blocking mode by launching the provisioning in the background and |
| * returning. The application should check the {@link DrmResult#getResultCode()} returned with |
| * {@link ListenableFuture} to proceed. |
| * <p> |
| * |
| * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved |
| * from the source through {#link getDrmInfo} or registering |
| * {@link PlayerCallback#onDrmInfo}. |
| * @return a {@link ListenableFuture} which represents the pending completion of the command. |
| * {@link DrmResult} will be delivered when the command completed. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| // This is an asynchronous call. |
| @NonNull |
| public ListenableFuture<DrmResult> prepareDrm(@NonNull final UUID uuid) { |
| if (uuid == null) { |
| throw new NullPointerException("uuid shouldn't be null"); |
| } |
| PendingFuture<DrmResult> pendingFuture = new PendingFuture<DrmResult>(mExecutor) { |
| @Override |
| List<ResolvableFuture<DrmResult>> onExecute() { |
| ArrayList<ResolvableFuture<DrmResult>> futures = new ArrayList<>(); |
| ResolvableFuture<DrmResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.prepareDrm(uuid); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_PREPARE_DRM, future, token); |
| } |
| futures.add(future); |
| return futures; |
| } |
| }; |
| |
| addPendingFuture(pendingFuture); |
| return pendingFuture; |
| } |
| |
| /** |
| * Releases the DRM session |
| * <p> |
| * The player has to have an active DRM session and be in stopped, or prepared |
| * state before this call is made. |
| * A {@code reset()} call will release the DRM session implicitly. |
| * |
| * @throws NoDrmSchemeException if there is no active DRM session to release |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void releaseDrm() throws NoDrmSchemeException { |
| try { |
| mPlayer.releaseDrm(); |
| } catch (MediaPlayer2.NoDrmSchemeException e) { |
| throw new NoDrmSchemeException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * A key request/response exchange occurs between the app and a license server |
| * to obtain or release keys used to decrypt encrypted content. |
| * <p> |
| * getDrmKeyRequest() is used to obtain an opaque key request byte array that is |
| * delivered to the license server. The opaque key request byte array is returned |
| * in KeyRequest.data. The recommended URL to deliver the key request to is |
| * returned in KeyRequest.defaultUrl. |
| * <p> |
| * After the app has received the key request response from the server, |
| * it should deliver to the response to the DRM engine plugin using the method |
| * {@link #provideDrmKeyResponse}. |
| * |
| * @param keySetId is the key-set identifier of the offline keys being released when keyType is |
| * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when |
| * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. |
| * |
| * @param initData is the container-specific initialization data when the keyType is |
| * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is |
| * interpreted based on the mime type provided in the mimeType parameter. It could |
| * contain, for example, the content ID, key ID or other data obtained from the content |
| * metadata that is required in generating the key request. |
| * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null. |
| * |
| * @param mimeType identifies the mime type of the content |
| * |
| * @param keyType specifies the type of the request. The request may be to acquire |
| * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content |
| * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired |
| * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId. |
| * |
| * @param optionalParameters are included in the key request message to |
| * allow a client application to provide additional message parameters to the server. |
| * This may be {@code null} if no additional parameters are to be sent. |
| * |
| * @throws NoDrmSchemeException if there is no active DRM session |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| @NonNull |
| public MediaDrm.KeyRequest getDrmKeyRequest( |
| @Nullable byte[] keySetId, @Nullable byte[] initData, |
| @Nullable String mimeType, int keyType, |
| @Nullable Map<String, String> optionalParameters) |
| throws NoDrmSchemeException { |
| try { |
| return mPlayer.getDrmKeyRequest( |
| keySetId, initData, mimeType, keyType, optionalParameters); |
| } catch (MediaPlayer2.NoDrmSchemeException e) { |
| throw new NoDrmSchemeException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * A key response is received from the license server by the app, then it is |
| * provided to the DRM engine plugin using provideDrmKeyResponse. When the |
| * response is for an offline key request, a key-set identifier is returned that |
| * can be used to later restore the keys to a new session with the method |
| * {@link #restoreDrmKeys}. |
| * <p> |
| * When the response is for a streaming or release request, null is returned. |
| * |
| * @param keySetId When the response is for a release request, keySetId identifies |
| * the saved key associated with the release request (i.e., the same keySetId |
| * passed to the earlier {@link #getDrmKeyRequest} call. It MUST be null when the |
| * response is for either streaming or offline key requests. |
| * |
| * @param response the byte array response from the server |
| * |
| * @throws NoDrmSchemeException if there is no active DRM session |
| * @throws DeniedByServerException if the response indicates that the |
| * server rejected the request |
| * @hide |
| */ |
| @Nullable |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public byte[] provideDrmKeyResponse( |
| @Nullable byte[] keySetId, @NonNull byte[] response) |
| throws NoDrmSchemeException, DeniedByServerException { |
| try { |
| return mPlayer.provideDrmKeyResponse(keySetId, response); |
| } catch (MediaPlayer2.NoDrmSchemeException e) { |
| throw new NoDrmSchemeException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Restore persisted offline keys into a new session. keySetId identifies the |
| * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}. |
| * |
| * @param keySetId identifies the saved key set to restore |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void restoreDrmKeys(@NonNull byte[] keySetId) throws NoDrmSchemeException { |
| if (keySetId == null) { |
| throw new NullPointerException("keySetId shouldn't be null"); |
| } |
| try { |
| mPlayer.restoreDrmKeys(keySetId); |
| } catch (MediaPlayer2.NoDrmSchemeException e) { |
| throw new NoDrmSchemeException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Read a DRM engine plugin String property value, given the property name string. |
| * <p> |
| * @param propertyName the property name |
| * |
| * Standard fields names are: |
| * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION}, |
| * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS} |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| @NonNull |
| public String getDrmPropertyString(@NonNull String propertyName) throws NoDrmSchemeException { |
| if (propertyName == null) { |
| throw new NullPointerException("propertyName shouldn't be null"); |
| } |
| try { |
| return mPlayer.getDrmPropertyString(propertyName); |
| } catch (MediaPlayer2.NoDrmSchemeException e) { |
| throw new NoDrmSchemeException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Set a DRM engine plugin String property value. |
| * <p> |
| * @param propertyName the property name |
| * @param value the property value |
| * |
| * Standard fields names are: |
| * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION}, |
| * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS} |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void setDrmPropertyString(@NonNull String propertyName, @NonNull String value) |
| throws NoDrmSchemeException { |
| if (propertyName == null) { |
| throw new NullPointerException("propertyName shouldn't be null"); |
| } |
| if (value == null) { |
| throw new NullPointerException("value shouldn't be null"); |
| } |
| try { |
| mPlayer.setDrmPropertyString(propertyName, value); |
| } catch (MediaPlayer2.NoDrmSchemeException e) { |
| throw new NoDrmSchemeException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Register a callback to be invoked for configuration of the DRM object before |
| * the session is created. |
| * <p> |
| * The callback will be invoked synchronously during the execution |
| * of {@link #prepareDrm(UUID uuid)}. |
| * |
| * @param listener the callback that will be run |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void setOnDrmConfigHelper(@Nullable final OnDrmConfigHelper listener) { |
| mPlayer.setOnDrmConfigHelper(listener == null ? null : |
| new MediaPlayer2.OnDrmConfigHelper() { |
| @Override |
| public void onDrmConfig(MediaPlayer2 mp, MediaItem item) { |
| listener.onDrmConfig(MediaPlayer.this, item); |
| } |
| }); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void setState(@PlayerState final int state) { |
| boolean needToNotify = false; |
| synchronized (mStateLock) { |
| if (mState != state) { |
| mState = state; |
| needToNotify = true; |
| } |
| } |
| if (needToNotify) { |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onPlayerStateChanged(MediaPlayer.this, state); |
| } |
| }); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void setBufferingState(final MediaItem item, @BuffState final int state) { |
| Integer previousState; |
| synchronized (mStateLock) { |
| previousState = mMediaItemToBuffState.put(item, state); |
| } |
| if (previousState == null || previousState.intValue() != state) { |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onBufferingStateChanged(MediaPlayer.this, item, state); |
| } |
| }); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void notifySessionPlayerCallback(final SessionPlayerCallbackNotifier notifier) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return; |
| } |
| } |
| List<Pair<SessionPlayer.PlayerCallback, Executor>> callbacks = getCallbacks(); |
| for (Pair<SessionPlayer.PlayerCallback, Executor> pair : callbacks) { |
| final SessionPlayer.PlayerCallback callback = pair.first; |
| pair.second.execute(new Runnable() { |
| @Override |
| public void run() { |
| notifier.callCallback(callback); |
| } |
| }); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void notifyMediaPlayerCallback(final MediaPlayerCallbackNotifier notifier) { |
| synchronized (mStateLock) { |
| if (mClosed) { |
| return; |
| } |
| } |
| List<Pair<SessionPlayer.PlayerCallback, Executor>> callbacks = getCallbacks(); |
| for (Pair<SessionPlayer.PlayerCallback, Executor> pair : callbacks) { |
| if (pair.first instanceof PlayerCallback) { |
| final PlayerCallback callback = (PlayerCallback) pair.first; |
| pair.second.execute(new Runnable() { |
| @Override |
| public void run() { |
| notifier.callCallback(callback); |
| } |
| }); |
| } |
| } |
| } |
| |
| private interface SessionPlayerCallbackNotifier { |
| void callCallback(SessionPlayer.PlayerCallback callback); |
| } |
| |
| private interface MediaPlayerCallbackNotifier { |
| void callCallback(PlayerCallback callback); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| List<ResolvableFuture<PlayerResult>> setMediaItemsInternal( |
| @NonNull MediaItem curItem, @Nullable MediaItem nextItem) { |
| if (curItem == null) { |
| throw new NullPointerException("curItem shouldn't be null"); |
| } |
| boolean setMediaItemCalled; |
| synchronized (mPlaylistLock) { |
| setMediaItemCalled = mSetMediaItemCalled; |
| } |
| |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| if (setMediaItemCalled) { |
| futures.add(setNextMediaItemInternal(curItem)); |
| futures.add(skipToNextInternal()); |
| } else { |
| futures.add(setMediaItemInternal(curItem)); |
| } |
| |
| if (nextItem != null) { |
| futures.add(setNextMediaItemInternal(nextItem)); |
| } |
| return futures; |
| } |
| |
| private ResolvableFuture<PlayerResult> setMediaItemInternal(MediaItem item) { |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setMediaItem(item); |
| addPendingCommandLocked(MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE, future, token); |
| } |
| synchronized (mPlaylistLock) { |
| mSetMediaItemCalled = true; |
| } |
| return future; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ResolvableFuture<PlayerResult> setNextMediaItemInternal(MediaItem item) { |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setNextMediaItem(item); |
| addPendingCommandLocked( |
| MediaPlayer2.CALL_COMPLETED_SET_NEXT_DATA_SOURCE, future, token); |
| } |
| return future; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ResolvableFuture<PlayerResult> skipToNextInternal() { |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.skipToNext(); |
| addPendingCommandLocked( |
| MediaPlayer2.CALL_COMPLETED_SKIP_TO_NEXT, future, token); |
| } |
| return future; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ResolvableFuture<PlayerResult> setPlayerVolumeInternal(float volume) { |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| synchronized (mPendingCommands) { |
| Object token = mPlayer.setPlayerVolume(volume); |
| addPendingCommandLocked( |
| MediaPlayer2.CALL_COMPLETED_SET_PLAYER_VOLUME, future, token); |
| } |
| return future; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ResolvableFuture<PlayerResult> createFutureForClosed() { |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| future.set(new PlayerResult(RESULT_ERROR_INVALID_STATE, null)); |
| return future; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ResolvableFuture<PlayerResult> createFutureForResultCode(int resultCode) { |
| return createFutureForResultCode(resultCode, null); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ResolvableFuture<PlayerResult> createFutureForResultCode(int resultCode, MediaItem item) { |
| ResolvableFuture<PlayerResult> future = ResolvableFuture.create(); |
| future.set(new PlayerResult(resultCode, |
| item == null ? mPlayer.getCurrentMediaItem() : item)); |
| return future; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| List<ResolvableFuture<PlayerResult>> createFuturesForResultCode(int resultCode) { |
| return createFuturesForResultCode(resultCode, null); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| List<ResolvableFuture<PlayerResult>> createFuturesForResultCode(int resultCode, |
| MediaItem item) { |
| ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>(); |
| futures.add(createFutureForResultCode(resultCode, item)); |
| return futures; |
| } |
| |
| @SuppressWarnings({"GuardedBy", "WeakerAccess"}) /* synthetic access */ |
| void applyShuffleModeLocked() { |
| mShuffledList.clear(); |
| mShuffledList.addAll(mPlaylist.getCollection()); |
| if (mShuffleMode == SessionPlayer.SHUFFLE_MODE_ALL |
| || mShuffleMode == SessionPlayer.SHUFFLE_MODE_GROUP) { |
| Collections.shuffle(mShuffledList); |
| } |
| } |
| |
| /** |
| * Update mCurPlaylistItem and mNextPlaylistItem based on mCurrentShuffleIdx value. |
| * |
| * @return A pair contains the changed current item and next item. If current item or next item |
| * is not changed, Pair.first or Pair.second will be null. If current item and next item are the |
| * same, it will return null Pair. If non null Pair which contains two nulls, that means one of |
| * current and next item or both are changed to null. |
| */ |
| @SuppressWarnings({"GuardedBy", "WeakerAccess"}) /* synthetic access */ |
| Pair<MediaItem, MediaItem> updateAndGetCurrentNextItemIfNeededLocked() { |
| MediaItem changedCurItem = null; |
| MediaItem changedNextItem = null; |
| if (mCurrentShuffleIdx < 0) { |
| if (mCurPlaylistItem == null && mNextPlaylistItem == null) { |
| return null; |
| } |
| mCurPlaylistItem = null; |
| mNextPlaylistItem = null; |
| return new Pair<>(null, null); |
| } |
| if (!Objects.equals(mCurPlaylistItem, mShuffledList.get(mCurrentShuffleIdx))) { |
| changedCurItem = mCurPlaylistItem = mShuffledList.get(mCurrentShuffleIdx); |
| } |
| int nextShuffleIdx = mCurrentShuffleIdx + 1; |
| if (nextShuffleIdx >= mShuffledList.size()) { |
| if (mRepeatMode == REPEAT_MODE_ALL || mRepeatMode == REPEAT_MODE_GROUP) { |
| nextShuffleIdx = 0; |
| } else { |
| nextShuffleIdx = END_OF_PLAYLIST; |
| } |
| } |
| |
| if (nextShuffleIdx == END_OF_PLAYLIST) { |
| mNextPlaylistItem = null; |
| } else if (!Objects.equals(mNextPlaylistItem, mShuffledList.get(nextShuffleIdx))) { |
| changedNextItem = mNextPlaylistItem = mShuffledList.get(nextShuffleIdx); |
| } |
| |
| return (changedCurItem == null && changedNextItem == null) |
| ? null : new Pair<>(changedCurItem, changedNextItem); |
| } |
| |
| // Clamps value to [0, maxValue] |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static int clamp(int value, int maxValue) { |
| if (value < 0) { |
| return 0; |
| } |
| return (value > maxValue) ? maxValue : value; |
| } |
| |
| @SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */ |
| void handleCallComplete(MediaPlayer2 mp, final MediaItem item, int what, int status) { |
| PendingCommand expected; |
| synchronized (mPendingCommands) { |
| expected = mPendingCommands.pollFirst(); |
| } |
| if (expected == null) { |
| Log.i(TAG, "No matching call type for " + what + ". Possibly because of reset()."); |
| return; |
| } |
| |
| final TrackInfo trackInfo = expected.mTrackInfo; |
| if (what != expected.mCallType) { |
| Log.w(TAG, "Call type does not match. expeced:" + expected.mCallType |
| + " actual:" + what); |
| status = MediaPlayer2.CALL_STATUS_ERROR_UNKNOWN; |
| } |
| if (status == MediaPlayer2.CALL_STATUS_NO_ERROR) { |
| switch (what) { |
| case MediaPlayer2.CALL_COMPLETED_PREPARE: |
| case MediaPlayer2.CALL_COMPLETED_PAUSE: |
| setState(PLAYER_STATE_PAUSED); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_PLAY: |
| setState(PLAYER_STATE_PLAYING); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_SEEK_TO: |
| final long pos = getCurrentPosition(); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onSeekCompleted(MediaPlayer.this, pos); |
| } |
| }); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE: |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onCurrentMediaItemChanged(MediaPlayer.this, item); |
| } |
| }); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_SET_PLAYBACK_PARAMS: |
| // TODO: Need to check if the speed value is really changed. |
| final float speed = mPlayer.getPlaybackParams().getSpeed(); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback( |
| SessionPlayer.PlayerCallback callback) { |
| callback.onPlaybackSpeedChanged(MediaPlayer.this, speed); |
| } |
| }); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_SET_AUDIO_ATTRIBUTES: |
| final AudioAttributesCompat attr = mPlayer.getAudioAttributes(); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onAudioAttributesChanged(MediaPlayer.this, attr); |
| } |
| }); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_SELECT_TRACK: |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onTrackSelected(MediaPlayer.this, |
| createTrackInfoInternal(trackInfo)); |
| } |
| }); |
| break; |
| case MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK: |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onTrackDeselected(MediaPlayer.this, |
| createTrackInfoInternal(trackInfo)); |
| } |
| }); |
| break; |
| } |
| } |
| if (what != MediaPlayer2.CALL_COMPLETED_PREPARE_DRM) { |
| Integer resultCode = sResultCodeMap.containsKey(status) |
| ? sResultCodeMap.get(status) : RESULT_ERROR_UNKNOWN; |
| expected.mFuture.set(new PlayerResult(resultCode, item)); |
| } else { |
| Integer resultCode = sPrepareDrmStatusMap.containsKey(status) |
| ? sPrepareDrmStatusMap.get(status) : DrmResult.RESULT_ERROR_PREPARATION_ERROR; |
| expected.mFuture.set(new DrmResult(resultCode, item)); |
| } |
| executePendingFutures(); |
| } |
| |
| private void executePendingFutures() { |
| synchronized (mPendingFutures) { |
| Iterator<PendingFuture<? super PlayerResult>> it = mPendingFutures.iterator(); |
| while (it.hasNext()) { |
| PendingFuture f = it.next(); |
| if (f.isCancelled() || f.execute()) { |
| mPendingFutures.removeFirst(); |
| } else { |
| break; |
| } |
| } |
| // Execute skip futures earlier for making them be skipped. |
| while (it.hasNext()) { |
| PendingFuture f = it.next(); |
| if (!f.mIsSeekTo) { |
| break; |
| } |
| f.execute(); |
| } |
| } |
| } |
| |
| SessionPlayer.TrackInfo createTrackInfoInternal(TrackInfo info) { |
| if (info == null) { |
| return null; |
| } |
| return new SessionPlayer.TrackInfo(info.getId(), info.getMediaItem(), info.getTrackType(), |
| info.getFormat()); |
| } |
| |
| private TrackInfo createTrackInfo(SessionPlayer.TrackInfo info) { |
| if (info == null) { |
| return null; |
| } |
| return new TrackInfo(info.getId(), info.getMediaItem(), info.getTrackType(), |
| info.getFormat()); |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| class Mp2DrmCallback extends MediaPlayer2.DrmEventCallback { |
| @Override |
| public void onDrmInfo( |
| MediaPlayer2 mp, final MediaItem item, final MediaPlayer2.DrmInfo drmInfo) { |
| notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(PlayerCallback callback) { |
| callback.onDrmInfo(MediaPlayer.this, item, |
| drmInfo == null ? null : new DrmInfo(drmInfo)); |
| } |
| }); |
| } |
| |
| @Override |
| public void onDrmPrepared(MediaPlayer2 mp, final MediaItem item, final int status) { |
| handleCallComplete(mp, item, MediaPlayer2.CALL_COMPLETED_PREPARE_DRM, status); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| class Mp2Callback extends MediaPlayer2.EventCallback { |
| @Override |
| public void onVideoSizeChanged( |
| MediaPlayer2 mp, final MediaItem item, final int width, final int height) { |
| final androidx.media2.common.VideoSize commonSize = |
| new androidx.media2.common.VideoSize(width, height); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onVideoSizeChangedInternal(MediaPlayer.this, item, commonSize); |
| } |
| }); |
| } |
| |
| @Override |
| public void onTimedMetaDataAvailable( |
| MediaPlayer2 mp, final MediaItem item, final TimedMetaData data) { |
| notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(PlayerCallback callback) { |
| callback.onTimedMetaDataAvailable(MediaPlayer.this, item, data); |
| } |
| }); |
| } |
| |
| @Override |
| public void onError( |
| MediaPlayer2 mp, final MediaItem item, final int what, final int extra) { |
| setState(PLAYER_STATE_ERROR); |
| setBufferingState(item, BUFFERING_STATE_UNKNOWN); |
| notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(PlayerCallback callback) { |
| callback.onError(MediaPlayer.this, item, what, extra); |
| } |
| }); |
| } |
| |
| @Override |
| public void onInfo( |
| MediaPlayer2 mp, final MediaItem item, final int mp2What, final int extra) { |
| switch (mp2What) { |
| case MediaPlayer2.MEDIA_INFO_BUFFERING_START: |
| setBufferingState(item, BUFFERING_STATE_BUFFERING_AND_STARVED); |
| break; |
| case MediaPlayer2.MEDIA_INFO_PREPARED: |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onTrackInfoChanged(MediaPlayer.this, getTrackInfoInternal()); |
| } |
| }); |
| setBufferingState(item, BUFFERING_STATE_BUFFERING_AND_PLAYABLE); |
| break; |
| case MediaPlayer2.MEDIA_INFO_BUFFERING_END: |
| setBufferingState(item, BUFFERING_STATE_BUFFERING_AND_PLAYABLE); |
| break; |
| case MediaPlayer2.MEDIA_INFO_BUFFERING_UPDATE: |
| if (extra /* percent */ >= 100) { |
| setBufferingState(item, BUFFERING_STATE_COMPLETE); |
| } |
| break; |
| case MediaPlayer2.MEDIA_INFO_DATA_SOURCE_LIST_END: |
| setState(PLAYER_STATE_PAUSED); |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onPlaybackCompleted(MediaPlayer.this); |
| } |
| }); |
| break; |
| case MediaPlayer2.MEDIA_INFO_METADATA_UPDATE: |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| callback.onTrackInfoChanged(MediaPlayer.this, getTrackInfoInternal()); |
| } |
| }); |
| break; |
| } |
| if (sInfoCodeMap.containsKey(mp2What)) { |
| final int what = sInfoCodeMap.get(mp2What); |
| notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(PlayerCallback callback) { |
| callback.onInfo(MediaPlayer.this, item, what, extra); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onCallCompleted( |
| MediaPlayer2 mp, final MediaItem item, int what, int status) { |
| handleCallComplete(mp, item, what, status); |
| } |
| |
| @Override |
| public void onMediaTimeDiscontinuity( |
| MediaPlayer2 mp, final MediaItem item, final MediaTimestamp timestamp) { |
| notifyMediaPlayerCallback(new MediaPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(PlayerCallback callback) { |
| callback.onMediaTimeDiscontinuity(MediaPlayer.this, item, timestamp); |
| } |
| }); |
| } |
| |
| @Override |
| public void onCommandLabelReached(MediaPlayer2 mp, Object label) { |
| // Ignore. MediaPlayer does not use MediaPlayer2.notifyWhenCommandLabelReached(). |
| } |
| |
| @Override |
| public void onSubtitleData(@NonNull MediaPlayer2 mp, final @NonNull MediaItem item, |
| final int trackIdx, final @NonNull SubtitleData data) { |
| notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() { |
| @Override |
| public void callCallback(SessionPlayer.PlayerCallback callback) { |
| SessionPlayer.TrackInfo track = createTrackInfoInternal(getTrackInfo(trackIdx)); |
| callback.onSubtitleData(MediaPlayer.this, item, track, data); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Interface definition for callbacks to be invoked when the player has the corresponding |
| * events. |
| */ |
| public abstract static class PlayerCallback extends SessionPlayer.PlayerCallback { |
| /** |
| * Called to indicate the video size |
| * <p> |
| * The video size (width and height) could be 0 if there was no video, |
| * no display surface was set, or the value was not determined yet. |
| * |
| * @param mp the player associated with this callback |
| * @param item the MediaItem of this media item |
| * @param size the size of the video |
| */ |
| public void onVideoSizeChanged( |
| @NonNull MediaPlayer mp, @NonNull MediaItem item, @NonNull VideoSize size) { } |
| |
| /** |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| @Override |
| public void onVideoSizeChangedInternal( |
| @NonNull SessionPlayer player, @NonNull MediaItem item, |
| @NonNull androidx.media2.common.VideoSize sizeInternal) { |
| if (!(player instanceof MediaPlayer)) { |
| throw new IllegalArgumentException("player must be MediaPlayer"); |
| } |
| VideoSize size = new VideoSize(sizeInternal); |
| onVideoSizeChanged((MediaPlayer) player, item, size); |
| } |
| |
| /** |
| * Called to indicate available timed metadata |
| * <p> |
| * This method will be called as timed metadata is extracted from the media, |
| * in the same order as it occurs in the media. The timing of this event is |
| * not controlled by the associated timestamp. |
| * <p> |
| * Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates |
| * {@link TimedMetaData}. |
| * |
| * @see TimedMetaData |
| * |
| * @param mp the player associated with this callback |
| * @param item the MediaItem of this media item |
| * @param data the timed metadata sample associated with this event |
| */ |
| public void onTimedMetaDataAvailable(@NonNull MediaPlayer mp, |
| @NonNull MediaItem item, @NonNull TimedMetaData data) { } |
| |
| /** |
| * Called to indicate an error. |
| * |
| * @param mp the MediaPlayer2 the error pertains to |
| * @param item the MediaItem of this media item |
| * @param what the type of error that has occurred. |
| * @param extra an extra code, specific to the error. Typically |
| * implementation dependent. |
| */ |
| public void onError(@NonNull MediaPlayer mp, |
| @NonNull MediaItem item, @MediaError int what, int extra) { } |
| |
| /** |
| * Called to indicate an info or a warning. |
| * |
| * @param mp the player the info pertains to. |
| * @param item the MediaItem of this media item |
| * @param what the type of info or warning. |
| * @param extra an extra code, specific to the info. Typically |
| * implementation dependent. |
| */ |
| public void onInfo(@NonNull MediaPlayer mp, |
| @NonNull MediaItem item, @MediaInfo int what, int extra) { } |
| |
| /** |
| * Called when a discontinuity in the normal progression of the media time is detected. |
| * <p> |
| * The "normal progression" of media time is defined as the expected increase of the |
| * playback position when playing media, relative to the playback speed (for instance every |
| * second, media time increases by two seconds when playing at 2x).<br> |
| * Discontinuities are encountered in the following cases: |
| * <ul> |
| * <li>when the player is starved for data and cannot play anymore</li> |
| * <li>when the player encounters a playback error</li> |
| * <li>when the a seek operation starts, and when it's completed</li> |
| * <li>when the playback speed changes</li> |
| * <li>when the playback state changes</li> |
| * <li>when the player is reset</li> |
| * </ul> |
| * |
| * @param mp the player the media time pertains to. |
| * @param item the MediaItem of this media item |
| * @param timestamp the timestamp that correlates media time, system time and clock rate, |
| * or {@link MediaTimestamp#TIMESTAMP_UNKNOWN} in an error case. |
| */ |
| public void onMediaTimeDiscontinuity(@NonNull MediaPlayer mp, |
| @NonNull MediaItem item, @NonNull MediaTimestamp timestamp) { } |
| |
| /** |
| * Called to indicate DRM info is available |
| * |
| * @param mp the {@code MediaPlayer2} associated with this callback |
| * @param item the MediaItem of this media item |
| * @param drmInfo DRM info of the source including PSSH, and subset |
| * of crypto schemes supported by this device |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public void onDrmInfo(@NonNull MediaPlayer mp, |
| @NonNull MediaItem item, @NonNull DrmInfo drmInfo) { } |
| } |
| |
| /** |
| * Class for the player to return each audio/video/subtitle track's metadata. |
| * |
| * @see #getTrackInfo |
| */ |
| public static final class TrackInfo { |
| public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; |
| public static final int MEDIA_TRACK_TYPE_VIDEO = 1; |
| public static final int MEDIA_TRACK_TYPE_AUDIO = 2; |
| public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; |
| public static final int MEDIA_TRACK_TYPE_METADATA = 5; |
| |
| /** |
| * @hide |
| */ |
| @IntDef(flag = false, /*prefix = "MEDIA_TRACK_TYPE",*/ value = { |
| MEDIA_TRACK_TYPE_UNKNOWN, |
| MEDIA_TRACK_TYPE_VIDEO, |
| MEDIA_TRACK_TYPE_AUDIO, |
| MEDIA_TRACK_TYPE_SUBTITLE, |
| MEDIA_TRACK_TYPE_METADATA, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public @interface MediaTrackType {} |
| |
| private final int mId; |
| private final MediaItem mItem; |
| private final int mTrackType; |
| private final MediaFormat mFormat; |
| |
| /** |
| * Gets the track type. |
| * @return TrackType which indicates if the track is video, audio, subtitle or metadata. |
| */ |
| public @MediaTrackType int getTrackType() { |
| return mTrackType; |
| } |
| |
| /** |
| * Gets the language code of the track. |
| * @return {@link Locale} which includes the language information. |
| */ |
| @NonNull |
| public Locale getLanguage() { |
| String language = mFormat != null ? mFormat.getString(MediaFormat.KEY_LANGUAGE) : null; |
| if (language == null) { |
| language = "und"; |
| } |
| return new Locale(language); |
| } |
| |
| /** |
| * Gets the {@link MediaFormat} of the track. If the format is |
| * unknown or could not be determined, null is returned. |
| */ |
| @Nullable |
| public MediaFormat getFormat() { |
| if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) { |
| return mFormat; |
| } |
| return null; |
| } |
| |
| int getId() { |
| return mId; |
| } |
| |
| MediaItem getMediaItem() { |
| return mItem; |
| } |
| |
| /** @hide */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public TrackInfo(int id, MediaItem item, int type, MediaFormat format) { |
| mId = id; |
| mItem = item; |
| mTrackType = type; |
| mFormat = format; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder out = new StringBuilder(128); |
| out.append(getClass().getName()); |
| out.append('#').append(mId); |
| out.append('{'); |
| switch (mTrackType) { |
| case MEDIA_TRACK_TYPE_VIDEO: |
| out.append("VIDEO"); |
| break; |
| case MEDIA_TRACK_TYPE_AUDIO: |
| out.append("AUDIO"); |
| break; |
| case MEDIA_TRACK_TYPE_SUBTITLE: |
| out.append("SUBTITLE"); |
| break; |
| default: |
| out.append("UNKNOWN"); |
| break; |
| } |
| out.append(", ").append(mFormat); |
| out.append("}"); |
| return out.toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + mId; |
| int hashCode = 0; |
| if (mItem != null) { |
| if (mItem.getMediaId() != null) { |
| hashCode = mItem.getMediaId().hashCode(); |
| } else { |
| hashCode = mItem.hashCode(); |
| } |
| } |
| result = prime * result + hashCode; |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null) { |
| return false; |
| } |
| if (getClass() != obj.getClass()) { |
| return false; |
| } |
| TrackInfo other = (TrackInfo) obj; |
| if (mId != other.mId) { |
| return false; |
| } |
| if (mItem == null && other.mItem == null) { |
| return true; |
| } else if (mItem == null || other.mItem == null) { |
| return false; |
| } else { |
| String mediaId = mItem.getMediaId(); |
| if (mediaId != null) { |
| return mediaId.equals(other.mItem.getMediaId()); |
| } |
| return mItem.equals(other.mItem); |
| } |
| } |
| } |
| |
| /** |
| * Encapsulates the DRM properties of the source. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final class DrmInfo { |
| private final MediaPlayer2.DrmInfo mMp2DrmInfo; |
| |
| /** |
| * Returns the PSSH info of the media item for each supported DRM scheme. |
| */ |
| @NonNull |
| public Map<UUID, byte[]> getPssh() { |
| return mMp2DrmInfo.getPssh(); |
| } |
| |
| /** |
| * Returns the intersection of the media item and the device DRM schemes. |
| * It effectively identifies the subset of the source's DRM schemes which |
| * are supported by the device too. |
| */ |
| @NonNull |
| public List<UUID> getSupportedSchemes() { |
| return mMp2DrmInfo.getSupportedSchemes(); |
| } |
| |
| DrmInfo(MediaPlayer2.DrmInfo info) { |
| mMp2DrmInfo = info; |
| } |
| }; |
| |
| /** |
| * Interface definition of a callback to be invoked when the app |
| * can do DRM configuration (get/set properties) before the session |
| * is opened. This facilitates configuration of the properties, like |
| * 'securityLevel', which has to be set after DRM scheme creation but |
| * before the DRM session is opened. |
| * <p> |
| * The only allowed DRM calls in this listener are {@link #getDrmPropertyString} |
| * and {@link #setDrmPropertyString}. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public interface OnDrmConfigHelper { |
| /** |
| * Called to give the app the opportunity to configure DRM before the session is created |
| * |
| * @param mp the {@code MediaPlayer} associated with this callback |
| * @param item the MediaItem of this media item |
| */ |
| void onDrmConfig(@NonNull MediaPlayer mp, @NonNull MediaItem item); |
| } |
| |
| /** |
| * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm(). |
| * Extends MediaDrm.MediaDrmException |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static class NoDrmSchemeException extends MediaDrmException { |
| public NoDrmSchemeException(@Nullable String detailMessage) { |
| super(detailMessage); |
| } |
| } |
| |
| /** |
| * Definitions for the metrics that are reported via the {@link #getMetrics} call. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static final class MetricsConstants { |
| private MetricsConstants() {} |
| |
| /** |
| * Key to extract the MIME type of the video track |
| * from the {@link #getMetrics} return value. |
| * The value is a String. |
| */ |
| public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime"; |
| |
| /** |
| * Key to extract the codec being used to decode the video track |
| * from the {@link #getMetrics} return value. |
| * The value is a String. |
| */ |
| public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec"; |
| |
| /** |
| * Key to extract the width (in pixels) of the video track |
| * from the {@link #getMetrics} return value. |
| * The value is an integer. |
| */ |
| public static final String WIDTH = "android.media.mediaplayer.width"; |
| |
| /** |
| * Key to extract the height (in pixels) of the video track |
| * from the {@link #getMetrics} return value. |
| * The value is an integer. |
| */ |
| public static final String HEIGHT = "android.media.mediaplayer.height"; |
| |
| /** |
| * Key to extract the count of video frames played |
| * from the {@link #getMetrics} return value. |
| * The value is an integer. |
| */ |
| public static final String FRAMES = "android.media.mediaplayer.frames"; |
| |
| /** |
| * Key to extract the count of video frames dropped |
| * from the {@link #getMetrics} return value. |
| * The value is an integer. |
| */ |
| public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped"; |
| |
| /** |
| * Key to extract the MIME type of the audio track |
| * from the {@link #getMetrics} return value. |
| * The value is a String. |
| */ |
| public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime"; |
| |
| /** |
| * Key to extract the codec being used to decode the audio track |
| * from the {@link #getMetrics} return value. |
| * The value is a String. |
| */ |
| public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec"; |
| |
| /** |
| * Key to extract the duration (in milliseconds) of the |
| * media being played |
| * from the {@link #getMetrics} return value. |
| * The value is a long. |
| */ |
| public static final String DURATION = "android.media.mediaplayer.durationMs"; |
| |
| /** |
| * Key to extract the playing time (in milliseconds) of the |
| * media being played |
| * from the {@link #getMetrics} return value. |
| * The value is a long. |
| */ |
| public static final String PLAYING = "android.media.mediaplayer.playingMs"; |
| |
| /** |
| * Key to extract the count of errors encountered while |
| * playing the media |
| * from the {@link #getMetrics} return value. |
| * The value is an integer. |
| */ |
| public static final String ERRORS = "android.media.mediaplayer.err"; |
| |
| /** |
| * Key to extract an (optional) error code detected while |
| * playing the media |
| * from the {@link #getMetrics} return value. |
| * The value is an integer. |
| */ |
| public static final String ERROR_CODE = "android.media.mediaplayer.errcode"; |
| } |
| |
| /** |
| * Result class of the asynchronous DRM APIs. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public static class DrmResult extends PlayerResult { |
| /** |
| * The device required DRM provisioning but couldn't reach the provisioning server. |
| */ |
| public static final int RESULT_ERROR_PROVISIONING_NETWORK_ERROR = -1001; |
| |
| /** |
| * The device required DRM provisioning but the provisioning server denied the request. |
| */ |
| public static final int RESULT_ERROR_PROVISIONING_SERVER_ERROR = -1002; |
| |
| /** |
| * The DRM preparation has failed. |
| */ |
| public static final int RESULT_ERROR_PREPARATION_ERROR = -1003; |
| |
| /** |
| * The crypto scheme UUID that is not supported by the device. |
| */ |
| public static final int RESULT_ERROR_UNSUPPORTED_SCHEME = -1004; |
| |
| /** |
| * The hardware resources are not available, due to being in use. |
| */ |
| public static final int RESULT_ERROR_RESOURCE_BUSY = -1005; |
| |
| /** @hide */ |
| @IntDef(flag = false, /*prefix = "PREPARE_DRM_STATUS",*/ value = { |
| RESULT_SUCCESS, |
| RESULT_ERROR_PROVISIONING_NETWORK_ERROR, |
| RESULT_ERROR_PROVISIONING_SERVER_ERROR, |
| RESULT_ERROR_PREPARATION_ERROR, |
| RESULT_ERROR_UNSUPPORTED_SCHEME, |
| RESULT_ERROR_RESOURCE_BUSY, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public @interface DrmResultCode {} |
| |
| /** |
| * Constructor that uses the current system clock as the completion time. |
| * |
| * @param resultCode result code. Recommends to use the standard code defined here. |
| * @param item media item when the operation is completed |
| */ |
| public DrmResult(@DrmResultCode int resultCode, @NonNull MediaItem item) { |
| super(resultCode, item); |
| } |
| |
| /** |
| * Gets the result code. |
| * |
| * @return result code. |
| */ |
| @Override |
| @DrmResultCode |
| public int getResultCode() { |
| return super.getResultCode(); |
| } |
| } |
| |
| /** |
| * List for {@link MediaItem} which manages the resource life cycle of |
| * {@link android.os.ParcelFileDescriptor} in {@link FileMediaItem}. |
| */ |
| static class MediaItemList { |
| private ArrayList<MediaItem> mList = new ArrayList<>(); |
| |
| void add(int index, MediaItem item) { |
| if (item instanceof FileMediaItem) { |
| ((FileMediaItem) item).increaseRefCount(); |
| } |
| mList.add(index, item); |
| } |
| |
| boolean replaceAll(Collection<MediaItem> c) { |
| for (MediaItem item : c) { |
| if (item instanceof FileMediaItem) { |
| ((FileMediaItem) item).increaseRefCount(); |
| } |
| } |
| clear(); |
| return mList.addAll(c); |
| } |
| |
| MediaItem remove(int index) { |
| MediaItem item = mList.remove(index); |
| if (item instanceof FileMediaItem) { |
| ((FileMediaItem) item).decreaseRefCount(); |
| } |
| return item; |
| } |
| |
| MediaItem get(int index) { |
| return mList.get(index); |
| } |
| |
| MediaItem set(int index, MediaItem item) { |
| if (item instanceof FileMediaItem) { |
| ((FileMediaItem) item).increaseRefCount(); |
| } |
| MediaItem removed = mList.set(index, item); |
| if (removed instanceof FileMediaItem) { |
| ((FileMediaItem) removed).decreaseRefCount(); |
| } |
| return removed; |
| } |
| |
| void clear() { |
| for (MediaItem item : mList) { |
| if (item instanceof FileMediaItem) { |
| ((FileMediaItem) item).decreaseRefCount(); |
| } |
| } |
| mList.clear(); |
| } |
| |
| int size() { |
| return mList.size(); |
| } |
| |
| boolean contains(Object o) { |
| return mList.contains(o); |
| } |
| |
| int indexOf(Object o) { |
| return mList.indexOf(o); |
| } |
| |
| boolean isEmpty() { |
| return mList.isEmpty(); |
| } |
| |
| Collection<MediaItem> getCollection() { |
| return mList; |
| } |
| } |
| } |