Gradle dependencies
compile group: 'androidx.media2', name: 'media2-player', version: '1.3.0'
- groupId: androidx.media2
- artifactId: media2-player
- version: 1.3.0
Artifact androidx.media2:media2-player:1.3.0 it located at Google repository (https://maven.google.com/)
Overview
A media player which plays MediaItems. The details on playback control and player states
can be found in the documentation of the base class, SessionPlayer.
Topic covered here:
- Audio focus and noisy intent
Audio focus and noisy intent
By default, MediaPlayer handles audio focus and noisy intent with AudioAttributesCompat set to this player. You need to call MediaPlayer.setAudioAttributes(AudioAttributesCompat) set the audio attribute while in the SessionPlayer.PLAYER_STATE_IDLE.
Here's the table of automatic audio focus behavior with audio attributes.
If an AudioAttributesCompat is not specified by MediaPlayer.setAudioAttributes(AudioAttributesCompat), MediaPlayer.getAudioAttributes() will return null and the default audio focus behavior will follow
the null case on the table above.
For more information about the audio focus, take a look at Managing audio focus
Summary
Fields |
---|
public static final int | MEDIA_INFO_AUDIO_NOT_PLAYING Informs that audio is not playing. |
public static final int | MEDIA_INFO_BAD_INTERLEAVING 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. |
public static final int | MEDIA_INFO_BUFFERING_END The player is resuming playback after filling buffers. |
public static final int | MEDIA_INFO_BUFFERING_START The player is temporarily pausing playback internally in order to
buffer more data. |
public static final int | MEDIA_INFO_BUFFERING_UPDATE Update status in buffering a media source received through progressive downloading. |
public static final int | MEDIA_INFO_EXTERNAL_METADATA_UPDATE A new set of external-only metadata is available. |
public static final int | MEDIA_INFO_MEDIA_ITEM_END The player just completed the playback of this media item. |
public static final int | MEDIA_INFO_MEDIA_ITEM_LIST_END The player just completed the playback of all the media items set by MediaPlayer.setPlaylist(List, MediaMetadata)
and MediaPlayer.setMediaItem(MediaItem). |
public static final int | MEDIA_INFO_MEDIA_ITEM_REPEAT The player just completed an iteration of playback loop. |
public static final int | MEDIA_INFO_MEDIA_ITEM_START The player just started the playback of this media item. |
public static final int | MEDIA_INFO_METADATA_UPDATE A new set of metadata is available. |
public static final int | MEDIA_INFO_NETWORK_BANDWIDTH Estimated network bandwidth information (kbps) is available; currently this event fires
simultaneously as MediaPlayer.MEDIA_INFO_BUFFERING_START and MediaPlayer.MEDIA_INFO_BUFFERING_END
when playing network files. |
public static final int | MEDIA_INFO_NOT_SEEKABLE The media cannot be seeked (e.g live stream) |
public static final int | MEDIA_INFO_PREPARED The player just finished preparing a media item for playback. |
public static final int | MEDIA_INFO_SUBTITLE_TIMED_OUT Reading the subtitle track takes too long. |
public static final int | MEDIA_INFO_UNSUPPORTED_SUBTITLE Subtitle track was not supported by the media framework. |
public static final int | MEDIA_INFO_VIDEO_NOT_PLAYING Informs that video is not playing. |
public static final int | MEDIA_INFO_VIDEO_RENDERING_START The player just pushed the very first video frame for rendering. |
public static final int | MEDIA_INFO_VIDEO_TRACK_LAGGING The video is too complex for the decoder: it can't decode frames fast
enough. |
public static final int | NO_TRACK_SELECTED The return value of MediaPlayer.getSelectedTrack(int) when there is no selected track
for the given type. |
public static final int | PLAYER_ERROR_IO File or network related operation errors. |
public static final int | PLAYER_ERROR_MALFORMED Bitstream is not conforming to the related coding standard or file spec. |
public static final int | PLAYER_ERROR_TIMED_OUT Some operation takes too long to complete, usually more than 3-5 seconds. |
public static final int | PLAYER_ERROR_UNKNOWN Unspecified player error. |
public static final int | PLAYER_ERROR_UNSUPPORTED Bitstream is conforming to the related coding standard or file spec, but
the media framework does not support the feature. |
public static final int | SEEK_CLOSEST This mode is used with MediaPlayer.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. |
public static final int | SEEK_CLOSEST_SYNC This mode is used with MediaPlayer.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. |
public static final int | SEEK_NEXT_SYNC This mode is used with MediaPlayer.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. |
public static final int | SEEK_PREVIOUS_SYNC This mode is used with MediaPlayer.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. |
from SessionPlayer | BUFFERING_STATE_BUFFERING_AND_PLAYABLE, BUFFERING_STATE_BUFFERING_AND_STARVED, BUFFERING_STATE_COMPLETE, BUFFERING_STATE_UNKNOWN, INVALID_ITEM_INDEX, PLAYER_STATE_ERROR, PLAYER_STATE_IDLE, PLAYER_STATE_PAUSED, PLAYER_STATE_PLAYING, REPEAT_MODE_ALL, REPEAT_MODE_GROUP, REPEAT_MODE_NONE, REPEAT_MODE_ONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP, SHUFFLE_MODE_NONE, UNKNOWN_TIME |
Constructors |
---|
public | MediaPlayer(Context context)
Constructor to create a MediaPlayer instance. |
Methods |
---|
public <any> | addPlaylistItem(int index, MediaItem item)
Adds the media item to the playlist at the index. |
public <any> | attachAuxEffect(int effectId)
Attaches an auxiliary effect to the player. |
public void | close()
Closes the player and relinquish underlying resources. |
public <any> | deselectTrack(SessionPlayer.TrackInfo trackInfo)
Deselects the MediaPlayer.TrackInfo for the current media item. |
public abstract AudioAttributesCompat | getAudioAttributes()
Gets the AudioAttributesCompat that media player has. |
public int | getAudioSessionId()
Returns the audio session ID. |
public abstract long | getBufferedPosition()
Gets the position for how much has been buffered, or SessionPlayer.UNKNOWN_TIME if unknown. |
public abstract int | getBufferingState()
Returns the current buffering state of the player. |
public abstract MediaItem | getCurrentMediaItem()
Gets the current media item, which is currently playing or would be played with later
SessionPlayer.play(). |
public int | getCurrentMediaItemIndex()
Gets the index of current media item in playlist. |
public abstract long | getCurrentPosition()
Gets the current playback position. |
public MediaPlayer.DrmInfo | getDrmInfo()
Retrieves the DRM Info associated with the current media item. |
public MediaDrm.KeyRequest | getDrmKeyRequest(byte[] keySetId[], byte[] initData[], java.lang.String mimeType, int keyType, java.util.Map<java.lang.String, java.lang.String> optionalParameters)
A key request/response exchange occurs between the app and a license server
to obtain or release keys used to decrypt encrypted content. |
public java.lang.String | getDrmPropertyString(java.lang.String propertyName)
Read a DRM engine plugin String property value, given the property name string. |
public abstract long | getDuration()
Gets the duration of the current media item, or SessionPlayer.UNKNOWN_TIME if unknown. |
public float | getMaxPlayerVolume()
|
public PersistableBundle | getMetrics()
|
public int | getNextMediaItemIndex()
Gets the next item index in the playlist. |
public PlaybackParams | getPlaybackParams()
Gets the playback params, containing the current playback rate. |
public abstract float | getPlaybackSpeed()
Gets the actual playback speed to be used by the player when playing. |
public abstract int | getPlayerState()
Gets the current player state. |
public float | getPlayerVolume()
|
public abstract java.util.List<MediaItem> | getPlaylist()
Gets the playlist. |
public abstract MediaMetadata | getPlaylistMetadata()
Gets the playlist metadata. |
public int | getPreviousMediaItemIndex()
Gets the previous item index in the playlist. |
public abstract int | getRepeatMode()
Gets the repeat mode. |
public MediaPlayer.TrackInfo | getSelectedTrack(int trackType)
Returns the selected track for the given track type. |
public abstract int | getShuffleMode()
Gets the shuffle mode. |
public MediaTimestamp | getTimestamp()
Gets current playback position as a MediaTimestamp. |
public java.util.List<MediaPlayer.TrackInfo> | getTrackInfo()
|
public java.util.List<SessionPlayer.TrackInfo> | getTracks()
Gets the full list of selected and unselected tracks that the media contains. |
public VideoSize | getVideoSize()
Returns the size of the video. |
public <any> | movePlaylistItem(int fromIndex, int toIndex)
Moves the media item at fromIdx to toIdx in the playlist. |
public <any> | pause()
Pauses playback. |
public <any> | play()
Starts or resumes playback. |
public <any> | prepare()
Prepares the media items for playback. |
public <any> | prepareDrm(java.util.UUID uuid)
Prepares the DRM for the current media item. |
public byte[] | provideDrmKeyResponse(byte[] keySetId[], byte[] response[])
A key response is received from the license server by the app, then it is
provided to the DRM engine plugin using provideDrmKeyResponse. |
public void | registerPlayerCallback(java.util.concurrent.Executor executor, MediaPlayer.PlayerCallback callback)
Register MediaPlayer.PlayerCallback to listen changes. |
public void | releaseDrm()
Releases the DRM session |
public <any> | removePlaylistItem(int index)
Removes the media item from the playlist |
public <any> | replacePlaylistItem(int index, MediaItem item)
Replaces the media item at index in the playlist. |
public void | reset()
Resets MediaPlayer to its uninitialized state if not closed. |
public void | restoreDrmKeys(byte[] keySetId[])
Restore persisted offline keys into a new session. |
public <any> | seekTo(long position)
Seeks to the specified position. |
public <any> | seekTo(long position, int mode)
Moves the media to specified time position by considering the given mode. |
public <any> | selectTrack(MediaPlayer.TrackInfo trackInfo)
|
public <any> | selectTrack(SessionPlayer.TrackInfo trackInfo)
Selects the MediaPlayer.TrackInfo for the current media item. |
public <any> | setAudioAttributes(AudioAttributesCompat attributes)
Sets the AudioAttributesCompat to be used during the playback of the media. |
public <any> | setAudioSessionId(int sessionId)
Sets the audio session ID. |
public <any> | setAuxEffectSendLevel(float level)
Sets the send level of the player to the attached auxiliary effect. |
public void | setDrmPropertyString(java.lang.String propertyName, java.lang.String value)
Set a DRM engine plugin String property value. |
public <any> | setMediaItem(MediaItem item)
Sets a MediaItem for playback. |
public void | setOnDrmConfigHelper(MediaPlayer.OnDrmConfigHelper listener)
Register a callback to be invoked for configuration of the DRM object before
the session is created. |
public <any> | setPlaybackParams(PlaybackParams params)
Sets playback params using PlaybackParams. |
public <any> | setPlaybackSpeed(float playbackSpeed)
Sets the playback speed. |
public <any> | setPlayerVolume(float volume)
Sets the volume of the audio of the media to play, expressed as a linear multiplier
on the audio samples. |
public <any> | setPlaylist(java.util.List<MediaItem> list, MediaMetadata metadata)
Sets a list of MediaItem with metadata. |
public <any> | setRepeatMode(int repeatMode)
Sets the repeat mode. |
public <any> | setShuffleMode(int shuffleMode)
Sets the shuffle mode. |
public <any> | setSurface(Surface surface)
Sets the to be used as the sink for the video portion of
the media. |
public <any> | skipToNextPlaylistItem()
Skips to the next item in the playlist. |
public <any> | skipToPlaylistItem(int index)
Skips to the item in the playlist at the index. |
public <any> | skipToPreviousPlaylistItem()
Skips to the previous item in the playlist. |
public void | unregisterPlayerCallback(MediaPlayer.PlayerCallback callback)
Unregister the previously registered MediaPlayer.PlayerCallback. |
public <any> | updatePlaylistMetadata(MediaMetadata metadata)
Updates the playlist metadata while keeping the playlist as-is. |
from SessionPlayer | getCallbacks, registerPlayerCallback, unregisterPlayerCallback |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
PLAYER_ERROR_UNKNOWNUnspecified player error.
See also: MediaPlayer.PlayerCallback.onError(MediaPlayer, MediaItem, int, int)
public static final int
PLAYER_ERROR_IOFile or network related operation errors.
See also: MediaPlayer.PlayerCallback.onError(MediaPlayer, MediaItem, int, int)
public static final int
PLAYER_ERROR_MALFORMEDBitstream is not conforming to the related coding standard or file spec.
See also: MediaPlayer.PlayerCallback.onError(MediaPlayer, MediaItem, int, int)
public static final int
PLAYER_ERROR_UNSUPPORTEDBitstream is conforming to the related coding standard or file spec, but
the media framework does not support the feature.
See also: MediaPlayer.PlayerCallback.onError(MediaPlayer, MediaItem, int, int)
public static final int
PLAYER_ERROR_TIMED_OUTSome operation takes too long to complete, usually more than 3-5 seconds.
See also: MediaPlayer.PlayerCallback.onError(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_MEDIA_ITEM_STARTThe player just started the playback of this media item.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_VIDEO_RENDERING_STARTThe player just pushed the very first video frame for rendering.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_MEDIA_ITEM_ENDThe player just completed the playback of this media item.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_MEDIA_ITEM_LIST_ENDThe player just completed the playback of all the media items set by MediaPlayer.setPlaylist(List, MediaMetadata)
and MediaPlayer.setMediaItem(MediaItem).
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_MEDIA_ITEM_REPEATThe player just completed an iteration of playback loop. This event is sent only when
looping is enabled by MediaPlayer.setRepeatMode(int).
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_PREPAREDThe player just finished preparing a media item for playback.
See also: MediaPlayer.prepare(), MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_VIDEO_TRACK_LAGGINGThe video is too complex for the decoder: it can't decode frames fast
enough. Possibly only the audio plays fine at this stage.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_BUFFERING_STARTThe player is temporarily pausing playback internally in order to
buffer more data.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_BUFFERING_ENDThe player is resuming playback after filling buffers.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_NETWORK_BANDWIDTHEstimated network bandwidth information (kbps) is available; currently this event fires
simultaneously as MediaPlayer.MEDIA_INFO_BUFFERING_START and MediaPlayer.MEDIA_INFO_BUFFERING_END
when playing network files.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_BUFFERING_UPDATEUpdate 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.
The extra parameter in MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int) is the
percentage (0-100) of the content that has been buffered or played thus far.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_BAD_INTERLEAVINGBad 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 also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_NOT_SEEKABLEThe media cannot be seeked (e.g live stream)
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_METADATA_UPDATEA new set of metadata is available.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_EXTERNAL_METADATA_UPDATEA new set of external-only metadata is available. Used by
JAVA framework to avoid triggering track scanning.
public static final int
MEDIA_INFO_AUDIO_NOT_PLAYINGInforms that audio is not playing. Note that playback of the video
is not interrupted.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_VIDEO_NOT_PLAYINGInforms that video is not playing. Note that playback of the audio
is not interrupted.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_UNSUPPORTED_SUBTITLESubtitle track was not supported by the media framework.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
MEDIA_INFO_SUBTITLE_TIMED_OUTReading the subtitle track takes too long.
See also: MediaPlayer.PlayerCallback.onInfo(MediaPlayer, MediaItem, int, int)
public static final int
SEEK_PREVIOUS_SYNCThis mode is used with MediaPlayer.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 also: MediaPlayer.seekTo(long, int)
public static final int
SEEK_NEXT_SYNCThis mode is used with MediaPlayer.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 also: MediaPlayer.seekTo(long, int)
public static final int
SEEK_CLOSEST_SYNCThis mode is used with MediaPlayer.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 also: MediaPlayer.seekTo(long, int)
public static final int
SEEK_CLOSESTThis mode is used with MediaPlayer.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 also: MediaPlayer.seekTo(long, int)
public static final int
NO_TRACK_SELECTEDDeprecated: MediaPlayer.getSelectedTrack(int) returns null instead of this value.
The return value of MediaPlayer.getSelectedTrack(int) when there is no selected track
for the given type.
See also: MediaPlayer.getSelectedTrack(int)
Constructors
public
MediaPlayer(Context context)
Constructor to create a MediaPlayer instance.
Parameters:
context: A that will be used to resolve UriMediaItem.
Methods
Starts or resumes playback.
On success, this transfers the player state to SessionPlayer.PLAYER_STATE_PLAYING and
a would be returned with the current media item when
the command was completed. If it is called in SessionPlayer.PLAYER_STATE_IDLE or
SessionPlayer.PLAYER_STATE_ERROR, it would be ignored and a
would be returned with .
Returns:
a representing the pending completion of the command
Pauses playback.
On success, this transfers the player state to SessionPlayer.PLAYER_STATE_PAUSED and
a would be returned with the current media item when the
command was completed. If it is called in SessionPlayer.PLAYER_STATE_IDLE or
SessionPlayer.PLAYER_STATE_ERROR, it would be ignored and a
would be returned with .
Returns:
a representing the pending completion of the command
Prepares the media items for playback. Before calling this API, set media item(s) through
either MediaPlayer.setMediaItem(MediaItem) or MediaPlayer.setPlaylist(List, MediaMetadata), and set a display surface through
MediaPlayer.setSurface(Surface) when needed.
On success, this transfers the player state from SessionPlayer.PLAYER_STATE_IDLE to
SessionPlayer.PLAYER_STATE_PAUSED and a would be returned with
the prepared media item when the command completed. If it's not called in
SessionPlayer.PLAYER_STATE_IDLE, it would be ignored and would
be returned with .
Returns:
a representing the pending completion of the command
public <any>
seekTo(long position)
Seeks to the specified position.
The position is the relative position based on the MediaItem.getStartPosition(). So
calling MediaPlayer.seekTo(long) with 0 means the seek to the start position.
On success, a would be returned with the current media
item when the command completed. If it's called in SessionPlayer.PLAYER_STATE_IDLE, it is ignored
and a would be returned with
.
Parameters:
position: the new playback position in ms. The value would be in the range of start
and end positions defined in MediaItem.
Returns:
a representing the pending completion of the command
public <any>
setPlaybackSpeed(float playbackSpeed)
Sets the playback speed. The default playback speed is 1.0f, and values less than
or equals to 0.0f is not allowed.
On success, a would be returned with the current media
item when the command completed.
Parameters:
playbackSpeed: the requested playback speed
Returns:
a representing the pending completion of the command
See also: MediaPlayer.getPlaybackSpeed(), SessionPlayer.PlayerCallback.onPlaybackSpeedChanged(SessionPlayer, float)
Sets the AudioAttributesCompat to be used during the playback of the media.
You must call this method in SessionPlayer.PLAYER_STATE_IDLE in order for the audio attributes to
become effective thereafter. Otherwise, the call would be ignored and
would be returned with
.
On success, a would be returned with the current media
item when the command completed.
Parameters:
attributes: non-null AudioAttributes
.
public abstract int
getPlayerState()
Gets the current player state.
Returns:
the current player state
See also: SessionPlayer.PlayerCallback.onPlayerStateChanged(SessionPlayer, int), SessionPlayer.PLAYER_STATE_IDLE, SessionPlayer.PLAYER_STATE_PAUSED, SessionPlayer.PLAYER_STATE_PLAYING, SessionPlayer.PLAYER_STATE_ERROR
public abstract long
getCurrentPosition()
Gets the current playback position.
The position is the relative position based on the MediaItem.getStartPosition().
So the position 0 means the start position of the MediaItem.
Returns:
the current playback position in ms, or SessionPlayer.UNKNOWN_TIME if unknown
public abstract long
getDuration()
Gets the duration of the current media item, or SessionPlayer.UNKNOWN_TIME if unknown. If the
current MediaItem has either start or end position, then duration would be adjusted
accordingly instead of returning the whole size of the MediaItem.
Returns:
the duration in ms, or SessionPlayer.UNKNOWN_TIME if unknown
public abstract long
getBufferedPosition()
Gets the position for how much has been buffered, or SessionPlayer.UNKNOWN_TIME if unknown.
The position is the relative position based on the MediaItem.getStartPosition().
So the position 0 means the start position of the MediaItem.
Returns:
the buffered position in ms, or SessionPlayer.UNKNOWN_TIME if unknown
public abstract int
getBufferingState()
Returns the current buffering state of the player.
During the buffering, see SessionPlayer.getBufferedPosition() for the quantifying the amount
already buffered.
Returns:
the buffering state, or SessionPlayer.BUFFERING_STATE_UNKNOWN if unknown
See also: SessionPlayer.getBufferedPosition()
public abstract float
getPlaybackSpeed()
Gets the actual playback speed to be used by the player when playing. A value of 1.0f
is the default playback value, and a negative value indicates reverse playback.
Note that it may differ from the speed set in SessionPlayer.setPlaybackSpeed(float).
Returns:
the actual playback speed
Gets the AudioAttributesCompat that media player has.
Sets a MediaItem for playback. Use this or MediaPlayer.setPlaylist(List, MediaMetadata) to specify which
items to play. If you want to change current item in the playlist, use one of
MediaPlayer.skipToPlaylistItem(int), MediaPlayer.skipToNextPlaylistItem(), or
MediaPlayer.skipToPreviousPlaylistItem() instead of this method.
When this is called multiple times in any states other than SessionPlayer.PLAYER_STATE_ERROR, it
would override previous MediaPlayer.setMediaItem(MediaItem) or MediaPlayer.setPlaylist(List, MediaMetadata) calls.
It's recommended to fill MediaMetadata in MediaItem especially for the
duration information with the key MediaMetadata.METADATA_KEY_DURATION. Without the
duration information in the metadata, session will do extra work to get the duration and send
it to the controller.
On success, a would be returned with item set.
Parameters:
item: the descriptor of media item you want to play
Returns:
a which represents the pending completion of the command
See also: MediaPlayer.setPlaylist(List, MediaMetadata), SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata), SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem)
public <any>
setPlaylist(java.util.List<MediaItem> list,
MediaMetadata metadata)
Sets a list of MediaItem with metadata. Use this or MediaPlayer.setMediaItem(MediaItem) to specify
which items to play.
This can be called multiple times in any states other than SessionPlayer.PLAYER_STATE_ERROR. This
would override previous MediaPlayer.setMediaItem(MediaItem) or MediaPlayer.setPlaylist(List, MediaMetadata) calls.
Ensure uniqueness of each MediaItem in the playlist so the session can uniquely
identity individual items. All MediaItems wouldn't be null as well.
It's recommended to fill MediaMetadata in each MediaItem especially for the
duration information with the key MediaMetadata.METADATA_KEY_DURATION. Without the
duration information in the metadata, session will do extra work to get the duration and send
it to the controller.
On success, a would be returned with the first media item
of the playlist when the command completed.
Parameters:
list: a list of MediaItem objects to set as a play list
Returns:
a which represents the pending completion of the command
See also: MediaPlayer.setMediaItem(MediaItem), SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata), SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem)
public <any>
addPlaylistItem(int index,
MediaItem item)
Adds the media item to the playlist at the index. Index equals to or greater than
the current playlist size (e.g. MAX_VALUE
) will add the item at the end of
the playlist.
If index is less than or equal to the current index of the playlist,
the current index of the playlist would be increased correspondingly.
On success, a would be returned with item added.
Parameters:
index: the index of the item you want to add in the playlist
item: the media item you want to add
See also: SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata)
public <any>
removePlaylistItem(int index)
Removes the media item from the playlist
On success, a would be returned with item removed.
Parameters:
index: the index of the item you want to remove in the playlist
See also: SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata)
public <any>
replacePlaylistItem(int index,
MediaItem item)
Replaces the media item at index in the playlist. This can be also used to update metadata of
an item.
On success, a would be returned with item set.
Parameters:
index: the index of the item to replace in the playlist
item: the new item
See also: SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata)
public <any>
movePlaylistItem(int fromIndex, int toIndex)
Moves the media item at fromIdx to toIdx in the playlist.
On success, a would be returned with item set.
Parameters:
fromIndex: the media item's initial index in the playlist
toIndex: the media item's target index in the playlist
See also: SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata)
public <any>
skipToPreviousPlaylistItem()
Skips to the previous item in the playlist.
On success, a would be returned with the current media
item when the command completed.
Returns:
a representing the pending completion of the command
See also: SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem)
public <any>
skipToNextPlaylistItem()
Skips to the next item in the playlist.
On success, a would be returned with the current media
item when the command completed.
Returns:
a representing the pending completion of the command
See also: SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem)
public <any>
skipToPlaylistItem(int index)
Skips to the item in the playlist at the index.
On success, a would be returned with the current media
item when the command completed.
Parameters:
index: the index of the item you want to play in the playlist
See also: SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem)
Updates the playlist metadata while keeping the playlist as-is.
On success, a swuld be returned with the current media
item when the command completed.
Parameters:
metadata: metadata of the playlist
See also: SessionPlayer.PlayerCallback.onPlaylistMetadataChanged(SessionPlayer, MediaMetadata)
public <any>
setRepeatMode(int repeatMode)
Sets the repeat mode.
On success, a would be returned with the current media
item when the command completed.
Parameters:
repeatMode: repeat mode
See also: SessionPlayer.REPEAT_MODE_NONE, SessionPlayer.REPEAT_MODE_ONE, SessionPlayer.REPEAT_MODE_ALL, SessionPlayer.REPEAT_MODE_GROUP, SessionPlayer.PlayerCallback.onRepeatModeChanged(SessionPlayer, int)
public <any>
setShuffleMode(int shuffleMode)
Sets the shuffle mode.
On success, a would be returned with the current media
item when the command completed.
Parameters:
shuffleMode: the shuffle mode
Returns:
a representing the pending completion of the command
See also: SessionPlayer.SHUFFLE_MODE_NONE, SessionPlayer.SHUFFLE_MODE_ALL, SessionPlayer.SHUFFLE_MODE_GROUP, SessionPlayer.PlayerCallback.onShuffleModeChanged(SessionPlayer, int)
public abstract java.util.List<MediaItem>
getPlaylist()
Gets the playlist. It can be null if the playlist hasn't been set or it's reset by
SessionPlayer.setMediaItem(MediaItem).
Returns:
playlist, or null
See also: SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata)
Gets the playlist metadata.
Returns:
metadata metadata of the playlist, or null if none is set
See also: SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata), SessionPlayer.PlayerCallback.onPlaylistMetadataChanged(SessionPlayer, MediaMetadata)
public abstract int
getRepeatMode()
Gets the repeat mode.
Returns:
repeat mode
See also: SessionPlayer.REPEAT_MODE_NONE, SessionPlayer.REPEAT_MODE_ONE, SessionPlayer.REPEAT_MODE_ALL, SessionPlayer.REPEAT_MODE_GROUP, SessionPlayer.PlayerCallback.onRepeatModeChanged(SessionPlayer, int)
public abstract int
getShuffleMode()
Gets the shuffle mode.
Returns:
the shuffle mode
See also: SessionPlayer.SHUFFLE_MODE_NONE, SessionPlayer.SHUFFLE_MODE_ALL, SessionPlayer.SHUFFLE_MODE_GROUP, SessionPlayer.PlayerCallback.onShuffleModeChanged(SessionPlayer, int)
public abstract
MediaItem getCurrentMediaItem()
Gets the current media item, which is currently playing or would be played with later
SessionPlayer.play(). This value may be updated when
SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem) or
SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata) is
called.
Returns:
the current media item. Can be null only when the player is in
SessionPlayer.PLAYER_STATE_IDLE and a media item or playlist hasn't been set.
See also: SessionPlayer.setMediaItem(MediaItem), SessionPlayer.setPlaylist(List, MediaMetadata)
public int
getCurrentMediaItemIndex()
Gets the index of current media item in playlist. This value would be updated when
SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem) or
SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata) is called.
Returns:
the index of current media item. Can be SessionPlayer.INVALID_ITEM_INDEX when current
media item is null or not in the playlist, and when the playlist hasn't been set.
public int
getPreviousMediaItemIndex()
Gets the previous item index in the playlist. This value would be updated when
SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem) or
SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata) is called.
Returns:
the index of previous media item. Can be SessionPlayer.INVALID_ITEM_INDEX only when
previous media item does not exist or playlist hasn't been set.
public int
getNextMediaItemIndex()
Gets the next item index in the playlist. This value would be updated when
SessionPlayer.PlayerCallback.onCurrentMediaItemChanged(SessionPlayer, MediaItem) or
SessionPlayer.PlayerCallback.onPlaylistChanged(SessionPlayer, List, MediaMetadata) is called.
Returns:
the index of next media item. Can be SessionPlayer.INVALID_ITEM_INDEX only when next media
item does not exist or playlist hasn't been set.
Closes the player and relinquish underlying resources.
Resets 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 MediaPlayer.prepare().
Note that if the player is closed, there is no way to reuse the instance.
public <any>
setSurface(Surface surface)
Sets the to be used as the sink for the video portion of
the media.
A null surface will result in only the audio track being played.
If the Surface sends frames to a SurfaceTexture
, the timestamps
returned from SurfaceTexture
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.
On success, a is returned with
the current media item when the command completed.
Parameters:
surface: The to be used for the video portion of
the media.
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
public <any>
setPlayerVolume(float volume)
Sets the volume of the audio of the media to play, expressed as a linear multiplier
on the audio samples.
Note that this volume is specific to the player, and is separate from stream volume
used across the platform.
A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
gain. See MediaPlayer.getMaxPlayerVolume() for the volume range supported by this player.
The default player volume is 1.0f.
On success, a is returned with
the current media item when the command completed.
Parameters:
volume: a value between 0.0f and MediaPlayer.getMaxPlayerVolume().
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
public float
getPlayerVolume()
Returns:
the current volume of this player to this player. Note that it does not take into
account the associated stream volume.
public float
getMaxPlayerVolume()
Returns:
the maximum volume that can be used in MediaPlayer.setPlayerVolume(float).
Returns the size of the video.
Returns:
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 MediaPlayer.PlayerCallback can be registered via MediaPlayer.registerPlayerCallback(Executor, MediaPlayer.PlayerCallback) to
receive a notification MediaPlayer.PlayerCallback.onVideoSizeChanged(MediaPlayer, MediaItem, VideoSize) when the size
is available.
public PersistableBundle
getMetrics()
Returns:
a containing the set of attributes and values
available for the media being handled by this player instance.
The attributes are described in MediaPlayer.MetricsConstants.
Additional vendor-specific fields may also be present in the return value.
Sets playback params using PlaybackParams.
On success, a is returned with
the current media item when the command completed.
Parameters:
params: the playback params.
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
Gets the playback params, containing the current playback rate.
Returns:
the playback params.
public <any>
seekTo(long position, int mode)
Moves the media to specified time position by considering the given mode.
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.
On success, a is returned with
the current media item when the command completed.
Parameters:
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 MediaItem.
mode: the mode indicating where exactly to seek to.
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
Gets current playback position as a MediaTimestamp.
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.
To help users get current playback position, this method always anchors the timestamp
to the current system time
, so
MediaTimestamp.getAnchorMediaTimeUs() can be used as current playback position.
Returns:
a MediaTimestamp object if a timestamp is available, or null if no timestamp
is available, e.g. because the media player has not been initialized.
See also: MediaTimestamp
public <any>
setAudioSessionId(int sessionId)
Sets the audio session ID.
Parameters:
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.
This method must be called before MediaPlayer.setMediaItem(MediaItem) and MediaPlayer.setPlaylist(List, MediaMetadata) methods.
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
On success, a is returned with
the current media item when the command completed.
See also: AudioManager
public int
getAudioSessionId()
Returns the audio session ID.
Returns:
the audio session ID. See MediaPlayer.setAudioSessionId(int). Note that the audio
session ID is 0 if a problem occurred when the MediaPlayer was constructed or it is
closed.
public <any>
attachAuxEffect(int effectId)
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 MediaPlayer.setAuxEffectSendLevel(float).
After creating an auxiliary effect (e.g.
), retrieve its ID with
and use it when calling this method
to attach the player to the effect.
To detach the effect from the player, call this method with a null effect id.
This method must be called before MediaPlayer.setMediaItem(MediaItem) and MediaPlayer.setPlaylist(List, MediaMetadata) methods.
Parameters:
effectId: system wide unique id of the effect to attach
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
On success, a is returned with
the current media item when the command completed.
public <any>
setAuxEffectSendLevel(float level)
Sets the send level of the player to the attached auxiliary effect.
See MediaPlayer.attachAuxEffect(int). The level value range is 0 to 1.0.
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.
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)
On success, a is returned with
the current media item when the command completed.
Parameters:
level: send level scalar
Returns:
a which represents the pending completion of the command.
will be delivered when the command
completed.
public java.util.List<SessionPlayer.TrackInfo>
getTracks()
Gets the full list of selected and unselected tracks that the media contains. The order of
the list is irrelevant as different players expose tracks in different ways, but the tracks
will generally be ordered based on track type.
Returns:
list of tracks. The total number of tracks is the size of the list. If empty,
an empty list would be returned.
See also: SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA
public java.util.List<MediaPlayer.TrackInfo>
getTrackInfo()
Deprecated: Use MediaPlayer.getTracks() instead.
Returns the selected track for the given track type.
The return value is an element in the list returned by MediaPlayer.getTracks().
Parameters:
trackType: should be one of SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO,
SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE,
or SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA.
Returns:
metadata corresponding to the track currently selected for
playback; null is returned when there is no selected track for trackType or
when trackType is not one of audio or video.
See also: MediaPlayer.getTracks()
Selects the MediaPlayer.TrackInfo for the current media item.
If the player is in invalid state,
will be reported with
.
If a player is in Playing state, the selected track is presented immediately.
If a player is not in Playing state, it just marks the track to be played.
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.
The first audio and video tracks are selected by default if available, even though
this method is not called.
Currently, tracks that return true for SessionPlayer.TrackInfo.isSelectable() can be selected via
this method.
Parameters:
trackInfo: metadata corresponding to the track to be selected. A trackInfo
object can be obtained from MediaPlayer.getTracks().
Returns:
a which represents the pending completion of the command.
will be delivered when the command completed.
See also: MediaPlayer.getTracks()
Deprecated: Use MediaPlayer.selectTrack(SessionPlayer.TrackInfo) instead.
Deselects the MediaPlayer.TrackInfo for the current media item.
The track must be a subtitle track, and no audio or video tracks can be deselected.
Note: MediaPlayer.getSelectedTrack(int) returns the currently selected track per track type that
can be deselected, but the list may be invalidated when
SessionPlayer.PlayerCallback.onTracksChanged(SessionPlayer, List) is called.
Parameters:
trackInfo: the track to be selected
Returns:
a which represents the pending completion of the command
See also: SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA, MediaPlayer.PlayerCallback
Register MediaPlayer.PlayerCallback to listen changes.
Parameters:
executor: a callback Executor
callback: a PlayerCallback
Unregister the previously registered MediaPlayer.PlayerCallback.
Parameters:
callback: the callback to be removed
Retrieves the DRM Info associated with the current media item.
public <any>
prepareDrm(java.util.UUID uuid)
Prepares the DRM for the current media item.
If MediaPlayer.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
MediaPlayer.prepareDrm(UUID). It should be used only for a series of getDrmPropertyString
and setDrmPropertyString calls and refrain from any lengthy operation.
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 MediaPlayer.DrmResult.getResultCode() returned with
to proceed.
Parameters:
uuid: The UUID of the crypto scheme. If not known beforehand, it can be retrieved
from the source through {#link getDrmInfo} or registering
MediaPlayer.PlayerCallback.onDrmInfo(MediaPlayer, MediaItem, MediaPlayer.DrmInfo).
Returns:
a which represents the pending completion of the command.
MediaPlayer.DrmResult will be delivered when the command completed.
Releases the DRM session
The player has to have an active DRM session and be in stopped, or prepared
state before this call is made.
A reset() call will release the DRM session implicitly.
public MediaDrm.KeyRequest
getDrmKeyRequest(byte[] keySetId[], byte[] initData[], java.lang.String mimeType, int keyType, java.util.Map<java.lang.String, java.lang.String> optionalParameters)
A key request/response exchange occurs between the app and a license server
to obtain or release keys used to decrypt encrypted content.
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.
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
MediaPlayer.provideDrmKeyResponse(byte[], byte[]).
Parameters:
keySetId: is the key-set identifier of the offline keys being released when keyType is
MediaDrm
. It should be set to null for other key requests, when
keyType is MediaDrm
or MediaDrm
.
initData: is the container-specific initialization data when the keyType is
MediaDrm
or MediaDrm
. 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 MediaDrm
, it should be set to null.
mimeType: identifies the mime type of the content
keyType: specifies the type of the request. The request may be to acquire
keys for streaming, MediaDrm
, or for offline content
MediaDrm
, or to release previously acquired
keys (MediaDrm
), which are identified by a keySetId.
optionalParameters: are included in the key request message to
allow a client application to provide additional message parameters to the server.
This may be null if no additional parameters are to be sent.
public byte[]
provideDrmKeyResponse(byte[] keySetId[], byte[] response[])
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
MediaPlayer.restoreDrmKeys(byte[]).
When the response is for a streaming or release request, null is returned.
Parameters:
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 MediaPlayer.getDrmKeyRequest(byte[], byte[], String, int, Map) call. It MUST be null when the
response is for either streaming or offline key requests.
response: the byte array response from the server
public void
restoreDrmKeys(byte[] keySetId[])
Restore persisted offline keys into a new session. keySetId identifies the
keys to load, obtained from a prior call to MediaPlayer.provideDrmKeyResponse(byte[], byte[]).
Parameters:
keySetId: identifies the saved key set to restore
public java.lang.String
getDrmPropertyString(java.lang.String propertyName)
Read a DRM engine plugin String property value, given the property name string.
Parameters:
propertyName: the property name
Standard fields names are:
MediaDrm
, MediaDrm
,
MediaDrm
, MediaDrm
public void
setDrmPropertyString(java.lang.String propertyName, java.lang.String value)
Set a DRM engine plugin String property value.
Parameters:
propertyName: the property name
value: the property value
Standard fields names are:
MediaDrm
, MediaDrm
,
MediaDrm
, MediaDrm
Register a callback to be invoked for configuration of the DRM object before
the session is created.
The callback will be invoked synchronously during the execution
of MediaPlayer.prepareDrm(UUID).
Parameters:
listener: the callback that will be run
Source
/*
* 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;
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.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.collection.ArrayMap;
import androidx.concurrent.futures.AbstractResolvableFuture;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.ObjectsCompat;
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 com.google.common.util.concurrent.ListenableFuture;
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.Map;
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>
*
* <h3 id="AudioFocusAndNoisyIntent">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 summary="Audio focus handling overview">
* <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></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></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></td></tr>
* <tr><td><ul><li>{@link AudioAttributesCompat#USAGE_ASSISTANT}</li></ul></td>
* <td>{@link android.media.AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}</td>
* <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></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>
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
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;
/**
*/
@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)
public @interface MediaError {}
/**
* The player just started the playback of this media item.
* @see PlayerCallback#onInfo
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
public static final int MEDIA_INFO_BUFFERING_START = 701;
/**
* The player is resuming playback after filling buffers.
* @see PlayerCallback#onInfo
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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.
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
/**
* Reading the subtitle track takes too long.
* @see PlayerCallback#onInfo
*/
@RestrictTo(LIBRARY)
public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
/**
*/
@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)
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;
@IntDef(flag = false, /*prefix = "SEEK",*/ value = {
SEEK_PREVIOUS_SYNC,
SEEK_NEXT_SYNC,
SEEK_CLOSEST_SYNC,
SEEK_CLOSEST,
})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY)
public @interface SeekMode {}
/**
* The return value of {@link #getSelectedTrack(int)} when there is no selected track
* for the given type.
*
* @see #getSelectedTrack(int)
* @deprecated {@link #getSelectedTrack(int)} returns {@code null} instead of this value.
*/
@Deprecated
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 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;
@SuppressWarnings("WeakerAccess") /* synthetic access */
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<? extends PlayerResult> mFuture;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final SessionPlayer.TrackInfo mTrackInfo;
@SuppressWarnings("WeakerAccess") /* synthetic access */
PendingCommand(int callType, ResolvableFuture<? extends PlayerResult> future) {
this(callType, future, null);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
PendingCommand(int callType, ResolvableFuture<? extends PlayerResult> future,
SessionPlayer.TrackInfo trackInfo) {
mCallType = callType;
mFuture = future;
mTrackInfo = trackInfo;
}
@SuppressWarnings("unchecked")
<V extends PlayerResult> void setResult(V value) {
((ResolvableFuture<V>) mFuture).set(value);
}
}
/* 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() {
if (mFutures != null) {
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<? extends PlayerResult>> mPendingFutures = new ArrayDeque<>();
private final Object mStateLock = new Object();
@GuardedBy("mStateLock")
@PlayerState
private 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<? extends PlayerResult> 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<? extends PlayerResult> future,
final SessionPlayer.TrackInfo trackInfo, final Object token) {
final PendingCommand pendingCommand = new PendingCommand(callType, future, trackInfo);
mPendingCommands.add(pendingCommand);
addFutureListener(pendingCommand, future, token);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void addFutureListener(final PendingCommand pendingCommand,
final ResolvableFuture<? extends PlayerResult> 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("WeakerAccess") /* synthetic access */
void addPendingFuture(final PendingFuture<? extends PlayerResult> pendingFuture) {
synchronized (mPendingFutures) {
mPendingFutures.add(pendingFuture);
executePendingFutures();
}
}
/**
* Starts or resumes playback.
* <p>
* On success, this transfers the player state to {@link #PLAYER_STATE_PLAYING} and
* a {@link SessionPlayer.PlayerResult} would be returned with the current media item when
* the command was completed. If it is called in {@link #PLAYER_STATE_IDLE} or
* {@link #PLAYER_STATE_ERROR}, it would be ignored and a {@link SessionPlayer.PlayerResult}
* would be returned with {@link SessionPlayer.PlayerResult#RESULT_ERROR_INVALID_STATE}.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
*/
@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;
}
/**
* Pauses playback.
* <p>
* On success, this transfers the player state to {@link #PLAYER_STATE_PAUSED} and
* a {@link SessionPlayer.PlayerResult} would be returned with the current media item when the
* command was completed. If it is called in {@link #PLAYER_STATE_IDLE} or
* {@link #PLAYER_STATE_ERROR}, it would be ignored and a {@link SessionPlayer.PlayerResult}
* would be returned with {@link SessionPlayer.PlayerResult#RESULT_ERROR_INVALID_STATE}.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
*/
@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. Before calling this API, set media item(s) through
* either {@link #setMediaItem} or {@link #setPlaylist}, and set a display surface through
* {@link #setSurface} when needed.
* <p>
* On success, this transfers the player state from {@link #PLAYER_STATE_IDLE} to
* {@link #PLAYER_STATE_PAUSED} and a {@link SessionPlayer.PlayerResult} would be returned with
* the prepared media item when the command completed. If it's not called in
* {@link #PLAYER_STATE_IDLE}, it would be ignored and {@link SessionPlayer.PlayerResult} would
* be returned with {@link SessionPlayer.PlayerResult#RESULT_ERROR_INVALID_STATE}.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
*/
@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;
}
/**
* Seeks to the specified position.
* <p>
* The position is the relative position based on the {@link MediaItem#getStartPosition()}. So
* calling {@link #seekTo(long)} with {@code 0} means the seek to the start position.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed. If it's called in {@link #PLAYER_STATE_IDLE}, it is ignored
* and a {@link SessionPlayer.PlayerResult} would be returned with
* {@link SessionPlayer.PlayerResult#RESULT_ERROR_INVALID_STATE}.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @param position the new playback position in ms. The value would be in the range of start
* and end positions defined in {@link MediaItem}.
*/
@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. The default playback speed is {@code 1.0f}, and values less than
* or equals to {@code 0.0f} is not allowed.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @param playbackSpeed the requested playback speed
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #getPlaybackSpeed()
* @see PlayerCallback#onPlaybackSpeedChanged(SessionPlayer, float)
*/
@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;
}
/**
* Sets the {@link AudioAttributesCompat} to be used during the playback of the media.
* <p>
* You must call this method in {@link #PLAYER_STATE_IDLE} in order for the audio attributes to
* become effective thereafter. Otherwise, the call would be ignored and
* {@link SessionPlayer.PlayerResult} would be returned with
* {@link SessionPlayer.PlayerResult#RESULT_ERROR_INVALID_STATE}.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @param attributes non-null <code>AudioAttributes</code>.
*/
@Override
@NonNull
public ListenableFuture<PlayerResult> setAudioAttributes(
@NonNull final AudioAttributesCompat attributes) {
if (attributes == 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(attributes);
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;
}
}
/**
* Sets a {@link MediaItem} for playback. Use this or {@link #setPlaylist} to specify which
* items to play. If you want to change current item in the playlist, use one of
* {@link #skipToPlaylistItem}, {@link #skipToNextPlaylistItem}, or
* {@link #skipToPreviousPlaylistItem} instead of this method.
* <p>
* When this is called multiple times in any states other than {@link #PLAYER_STATE_ERROR}, it
* would override previous {@link #setMediaItem} or {@link #setPlaylist} calls.
* <p>
* It's recommended to fill {@link MediaMetadata} in {@link MediaItem} especially for the
* duration information with the key {@link MediaMetadata#METADATA_KEY_DURATION}. Without the
* duration information in the metadata, session will do extra work to get the duration and send
* it to the controller.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with {@code item} set.
*
* @param item the descriptor of media item you want to play
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see #setPlaylist
* @see PlayerCallback#onPlaylistChanged
* @see PlayerCallback#onCurrentMediaItemChanged
* @throws IllegalArgumentException if the given item is {@code 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();
mPlaylistMetadata = null;
mShuffledList.clear();
mCurPlaylistItem = item;
mNextPlaylistItem = null;
mCurrentShuffleIdx = END_OF_PLAYLIST;
}
notifySessionPlayerCallback(
callback -> callback.onPlaylistChanged(MediaPlayer.this, null, null));
futures.addAll(setMediaItemsInternal(item, null));
return futures;
}
};
addPendingFuture(pendingFuture);
return pendingFuture;
}
/**
* Sets a list of {@link MediaItem} with metadata. Use this or {@link #setMediaItem} to specify
* which items to play.
* <p>
* This can be called multiple times in any states other than {@link #PLAYER_STATE_ERROR}. This
* would override previous {@link #setMediaItem} or {@link #setPlaylist} calls.
* <p>
* Ensure uniqueness of each {@link MediaItem} in the playlist so the session can uniquely
* identity individual items. All {@link MediaItem}s wouldn't be {@code null} as well.
* <p>
* It's recommended to fill {@link MediaMetadata} in each {@link MediaItem} especially for the
* duration information with the key {@link MediaMetadata#METADATA_KEY_DURATION}. Without the
* duration information in the metadata, session will do extra work to get the duration and send
* it to the controller.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the first media item
* of the playlist when the command completed.
*
* @param list a list of {@link MediaItem} objects to set as a play list
* @throws IllegalArgumentException if the given list is {@code null} or empty, or has
* duplicated media items.
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see #setMediaItem
* @see PlayerCallback#onPlaylistChanged
* @see PlayerCallback#onCurrentMediaItemChanged
*/
@Override
@NonNull
public ListenableFuture<PlayerResult> setPlaylist(
@NonNull final List<MediaItem> list, @Nullable final MediaMetadata metadata) {
if (list == null) {
throw new NullPointerException("list shouldn't be null");
} else if (list.isEmpty()) {
throw new IllegalArgumentException("list shouldn't be empty");
}
synchronized (mStateLock) {
if (mClosed) {
return createFutureForClosed();
}
}
String errorString = null;
for (MediaItem item : list) {
if (item == null) {
errorString = "list 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 : list) {
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(list);
applyShuffleModeLocked();
mCurrentShuffleIdx = 0;
updateAndGetCurrentNextItemIfNeededLocked();
curItem = mCurPlaylistItem;
nextItem = mNextPlaylistItem;
}
notifySessionPlayerCallback(callback -> {
callback.onPlaylistChanged(MediaPlayer.this, list, metadata);
});
if (curItem != null) {
return setMediaItemsInternal(curItem, nextItem);
}
return createFuturesForResultCode(RESULT_SUCCESS);
}
};
addPendingFuture(pendingFuture);
return pendingFuture;
}
/**
* Adds the media item to the playlist at the index. Index equals to or greater than
* the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
* the playlist.
* <p>
* If index is less than or equal to the current index of the playlist,
* the current index of the playlist would be increased correspondingly.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with {@code item} added.
*
* @param index the index of the item you want to add in the playlist
* @param item the media item you want to add
* @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)
*/
@Override
@NonNull
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 == null || updatedCurNextItem.second == null) {
return createFuturesForResultCode(RESULT_SUCCESS);
}
ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
futures.add(setNextMediaItemInternal(updatedCurNextItem.second));
return futures;
}
};
addPendingFuture(pendingFuture);
return pendingFuture;
}
/**
* Removes the media item from the playlist
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with {@code item} removed.
*
* @param index the index of the item you want to remove in the playlist
* @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)
*/
@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 (curItem == null) {
resetInternal();
notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
@Override
public void callCallback(SessionPlayer.PlayerCallback callback) {
callback.onPlayerStateChanged(MediaPlayer.this, PLAYER_STATE_IDLE);
}
});
futures.add(createFutureForResultCode(RESULT_SUCCESS));
} else 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;
}
/**
* Replaces the media item at index in the playlist. This can be also used to update metadata of
* an item.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with {@code item} set.
*
* @param index the index of the item to replace in the playlist
* @param item the new item
* @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)
*/
@Override
@NonNull
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;
}
/**
* Moves the media item at {@code fromIdx} to {@code toIdx} in the playlist.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with {@code item} set.
*
* @param fromIndex the media item's initial index in the playlist
* @param toIndex the media item's target index in the playlist
* @see PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)
*/
@Override
@NonNull
public ListenableFuture<PlayerResult> movePlaylistItem(final int fromIndex, final int toIndex) {
if (fromIndex < 0 || toIndex < 0) {
throw new IllegalArgumentException("indices 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 (fromIndex >= mPlaylist.size() || toIndex >= mPlaylist.size()) {
return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE);
}
MediaItem item = mPlaylist.remove(fromIndex);
mPlaylist.add(toIndex, item);
if (mShuffleMode == SessionPlayer.SHUFFLE_MODE_NONE) {
mShuffledList.remove(fromIndex);
mShuffledList.add(toIndex, item);
if (item == mCurPlaylistItem) {
mCurrentShuffleIdx = toIndex;
}
}
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;
}
/**
* Skips to the previous item in the playlist.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)
*/
@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;
}
/**
* Skips to the next item in the playlist.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)
*/
@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;
}
/**
* Skips to the item in the playlist at the index.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @param index the index of the item you want to play in the playlist
* @see PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)
*/
@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;
}
/**
* Updates the playlist metadata while keeping the playlist as-is.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} swuld be returned with the current media
* item when the command completed.
*
* @param metadata metadata of the playlist
* @see PlayerCallback#onPlaylistMetadataChanged(SessionPlayer, MediaMetadata)
*/
@Override
@NonNull
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;
}
/**
* Sets the repeat mode.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @param repeatMode repeat mode
* @see #REPEAT_MODE_NONE
* @see #REPEAT_MODE_ONE
* @see #REPEAT_MODE_ALL
* @see #REPEAT_MODE_GROUP
* @see PlayerCallback#onRepeatModeChanged(SessionPlayer, int)
*/
@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;
}
/**
* Sets the shuffle mode.
* <p>
* On success, a {@link SessionPlayer.PlayerResult} would be returned with the current media
* item when the command completed.
*
* @param shuffleMode the shuffle mode
* @return a {@link ListenableFuture} representing the pending completion of the command
* @see #SHUFFLE_MODE_NONE
* @see #SHUFFLE_MODE_ALL
* @see #SHUFFLE_MODE_GROUP
* @see PlayerCallback#onShuffleModeChanged(SessionPlayer, int)
*/
@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;
applyShuffleModeLocked();
}
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();
}
/**
* Gets the index of current media item in playlist. This value would be updated when
* {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or
* {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is called.
*
* @return the index of current media item. Can be {@link #INVALID_ITEM_INDEX} when current
* media item is null or not in the playlist, and when the playlist hasn't been set.
*/
@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));
}
}
/**
* Gets the previous item index in the playlist. This value would be updated when
* {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or
* {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is called.
*
* @return the index of previous media item. Can be {@link #INVALID_ITEM_INDEX} only when
* previous media item does not exist or playlist hasn't been set.
*/
@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));
}
}
/**
* Gets the next item index in the playlist. This value would be updated when
* {@link PlayerCallback#onCurrentMediaItemChanged(SessionPlayer, MediaItem)} or
* {@link PlayerCallback#onPlaylistChanged(SessionPlayer, List, MediaMetadata)} is called.
*
* @return the index of next media item. Can be {@link #INVALID_ITEM_INDEX} only when next media
* item does not exist or playlist hasn't been set.
*/
@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));
}
}
/**
* Closes the player and relinquish underlying resources.
*/
@Override
public void close() {
super.close();
synchronized (mStateLock) {
if (!mClosed) {
mClosed = true;
reset();
mAudioFocusHandler.close();
mPlayer.close();
mExecutor.shutdown();
}
}
}
/**
* 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<? extends PlayerResult> f : mPendingFutures) {
if (f.mExecuteCalled && !f.isDone() && !f.isCancelled()) {
f.cancel(true);
}
}
mPendingFutures.clear();
}
resetInternal();
}
/**
* 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
@Override
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;
}
/**
* 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.
*/
@Override
@NonNull
public VideoSize getVideoSize() {
synchronized (mStateLock) {
if (mClosed) {
return new VideoSize(0, 0);
}
}
return new 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.
*/
@RestrictTo(LIBRARY)
@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 {@link #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) {
if (level < 0 || level > 1) {
// Returns ListenableFuture instead of throwing exception, not to newly throw an
// exception in existing code.
return createFutureForResultCode(RESULT_ERROR_BAD_VALUE);
}
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;
}
/**
* Gets the full list of selected and unselected tracks that the media contains. The order of
* the list is irrelevant as different players expose tracks in different ways, but the tracks
* will generally be ordered based on track type.
*
* @return list of tracks. The total number of tracks is the size of the list. If empty,
* an empty list would be returned.
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
*/
@Override
@NonNull
public List<SessionPlayer.TrackInfo> getTracks() {
synchronized (mStateLock) {
if (mClosed) {
return Collections.emptyList();
}
}
return mPlayer.getTracks();
}
/**
* @deprecated Use {@link #getTracks()} instead.
*/
@Deprecated
@NonNull
public List<TrackInfo> getTrackInfo() {
List<SessionPlayer.TrackInfo> infoInternals = getTracks();
List<TrackInfo> infos = new ArrayList<>();
for (SessionPlayer.TrackInfo infoInternal : infoInternals) {
infos.add(new TrackInfo(infoInternal));
}
return infos;
}
/**
* Returns the selected track for the given track type.
* The return value is an element in the list returned by {@link #getTracks()}.
*
* @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
* {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE},
* or {@link TrackInfo#MEDIA_TRACK_TYPE_METADATA}.
* @return metadata corresponding to the 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 #getTracks()
*/
@Override
@Nullable
public TrackInfo getSelectedTrack(@TrackInfo.MediaTrackType int trackType) {
synchronized (mStateLock) {
if (mClosed) {
return null;
}
}
SessionPlayer.TrackInfo infoInternal = mPlayer.getSelectedTrack(trackType);
return infoInternal == null ? null : new TrackInfo(infoInternal);
}
/**
* Selects the {@link TrackInfo} for the current media item.
* <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>
* 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>
* The first audio and video tracks are selected by default if available, even though
* this method is not called.
* <p>
* Currently, tracks that return true for {@link TrackInfo#isSelectable()} can be selected via
* this method.
*
* @param trackInfo metadata corresponding to the track to be selected. A {@code trackInfo}
* object can be obtained from {@link #getTracks()}.
*
* @see #getTracks
* @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> selectTrack(
@NonNull final SessionPlayer.TrackInfo trackInfo) {
if (trackInfo == null) {
throw new NullPointerException("trackInfo 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.selectTrack(trackInfo.getId());
addPendingCommandWithTrackInfoLocked(MediaPlayer2.CALL_COMPLETED_SELECT_TRACK,
future, trackInfo, token);
}
futures.add(future);
return futures;
}
};
addPendingFuture(pendingFuture);
return pendingFuture;
}
/**
* @deprecated Use {@link #selectTrack(SessionPlayer.TrackInfo)} instead.
*/
@Deprecated
@NonNull
public ListenableFuture<PlayerResult> selectTrack(@NonNull final TrackInfo trackInfo) {
return selectTrack((SessionPlayer.TrackInfo) trackInfo);
}
/**
* Deselects the {@link TrackInfo} for the current media item.
* <p>
* The track must be a subtitle track, and no audio or video tracks can be deselected.
* <p>
* Note: {@link #getSelectedTrack(int)} returns the currently selected track per track type that
* can be deselected, but the list may be invalidated when
* {@link PlayerCallback#onTracksChanged(SessionPlayer, List)} is called.
*
* @param trackInfo the track to be selected
* @return a {@link ListenableFuture} which represents the pending completion of the command
* @see TrackInfo#MEDIA_TRACK_TYPE_VIDEO
* @see TrackInfo#MEDIA_TRACK_TYPE_AUDIO
* @see TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE
* @see TrackInfo#MEDIA_TRACK_TYPE_METADATA
* @see PlayerCallback#onTrackDeselected(SessionPlayer, TrackInfo)
*/
@Override
@NonNull
public ListenableFuture<PlayerResult> deselectTrack(
@NonNull SessionPlayer.TrackInfo trackInfo) {
if (trackInfo == null) {
throw new NullPointerException("trackInfo 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.deselectTrack(trackInfo.getId());
addPendingCommandWithTrackInfoLocked(MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK,
future, trackInfo, token);
}
futures.add(future);
return futures;
}
};
addPendingFuture(pendingFuture);
return pendingFuture;
}
/**
* 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
*/
@Nullable
@RestrictTo(LIBRARY)
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.
*/
@RestrictTo(LIBRARY)
// 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
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
@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
*/
@Nullable
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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}
*/
@RestrictTo(LIBRARY)
@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}
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
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;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void resetInternal() {
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();
}
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 || mPlaylist.isEmpty()) {
if (mCurPlaylistItem == null && mNextPlaylistItem == null) {
return null;
}
mCurPlaylistItem = null;
mNextPlaylistItem = null;
return new Pair<>(null, null);
}
if (!ObjectsCompat.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 (!ObjectsCompat.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") /* 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;
}
if (what != expected.mCallType) {
Log.w(TAG, "Call type does not match. expected:" + 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:
case MediaPlayer2.CALL_COMPLETED_SKIP_TO_NEXT:
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, expected.mTrackInfo);
}
});
break;
case MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK:
notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
@Override
public void callCallback(SessionPlayer.PlayerCallback callback) {
callback.onTrackDeselected(MediaPlayer.this, expected.mTrackInfo);
}
});
break;
}
}
if (what != MediaPlayer2.CALL_COMPLETED_PREPARE_DRM) {
Integer resultCode = sResultCodeMap.containsKey(status)
? sResultCodeMap.get(status) : RESULT_ERROR_UNKNOWN;
expected.setResult(new PlayerResult(resultCode, item));
} else {
Integer resultCode = sPrepareDrmStatusMap.containsKey(status)
? sPrepareDrmStatusMap.get(status) : DrmResult.RESULT_ERROR_PREPARATION_ERROR;
expected.setResult(new DrmResult(resultCode, item));
}
executePendingFutures();
}
private void executePendingFutures() {
synchronized (mPendingFutures) {
Iterator<PendingFuture<? extends PlayerResult>> it = mPendingFutures.iterator();
while (it.hasNext()) {
PendingFuture<? extends PlayerResult> 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<? extends PlayerResult> f = it.next();
if (!f.mIsSeekTo) {
break;
}
f.execute();
}
}
}
@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) {
MediaItem currentItem = getCurrentMediaItem();
if (currentItem != null && currentItem == item) {
final androidx.media2.common.VideoSize commonSize =
new androidx.media2.common.VideoSize(width, height);
notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
@Override
public void callCallback(SessionPlayer.PlayerCallback callback) {
callback.onVideoSizeChanged(MediaPlayer.this, 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:
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_START:
boolean shouldNotifyCurrentMediaItemChanged;
MediaItem nextPlaylistItem;
synchronized (mPlaylistLock) {
if (mCurPlaylistItem == item) {
// Playback is started for the media item that the MediaPlayer has set
// as the current media item via MediaPlayer2.setMediaItem() or
// MediaPlayer2.skipToNext(). In that case, the current media item is
// already notified in the MediaPlayer2.EventCallback#onCallCompleted(),
// so don't need to notify again.
shouldNotifyCurrentMediaItemChanged = false;
nextPlaylistItem = null;
} else {
// Playback is advanced to the next item by MediaPlayer2 itself after
// the playback of the mCurPlaylistItem is completed.
// In that case, update the mCurPlaylistItem and also notify about the
// current media item changes.
shouldNotifyCurrentMediaItemChanged = true;
mCurrentShuffleIdx = mShuffledList.indexOf(item);
updateAndGetCurrentNextItemIfNeededLocked();
nextPlaylistItem = mNextPlaylistItem;
}
}
if (shouldNotifyCurrentMediaItemChanged) {
notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
@Override
public void callCallback(SessionPlayer.PlayerCallback callback) {
callback.onCurrentMediaItemChanged(MediaPlayer.this, item);
}
});
// If the playback is advanced to the next item by itself, then the next
// media item may be emptied. If so, sets the next media item so the
// playback continues.
if (nextPlaylistItem != null) {
PendingFuture<PlayerResult> pendingFuture =
new PendingFuture<PlayerResult>(mExecutor) {
@Override
List<ResolvableFuture<PlayerResult>> onExecute() {
ArrayList<ResolvableFuture<PlayerResult>> futures =
new ArrayList<>();
futures.add(setNextMediaItemInternal(nextPlaylistItem));
return futures;
}
};
addPendingFuture(pendingFuture);
}
}
break;
case MediaPlayer2.MEDIA_INFO_DATA_SOURCE_LIST_END:
MediaItem nextItemToPlay;
synchronized (mPlaylistLock) {
mCurrentShuffleIdx = mShuffledList.indexOf(item);
nextItemToPlay = mNextPlaylistItem;
}
if (nextItemToPlay != null) {
// Although the MediaPlayer2's playback is completed, but there's still
// remaining items to play in the playlist. It happens if the MediaPlayer2's
// playback is completed before the MediaPlayer has set the next item to
// play.
// Forcefully call skipToNextPlaylistItem to resume playback.
ListenableFuture<PlayerResult> future = skipToNextPlaylistItem();
if (future == null) {
Log.e(TAG, "Cannot play next media item", new IllegalStateException());
setState(PLAYER_STATE_ERROR);
}
} else {
// The playback for the playlist is completed for real. Notify accordingly.
setState(PLAYER_STATE_PAUSED);
notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
@Override
public void callCallback(SessionPlayer.PlayerCallback callback) {
callback.onPlaybackCompleted(MediaPlayer.this);
}
});
}
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, @NonNull final MediaItem item,
@NonNull final SessionPlayer.TrackInfo track, @NonNull final SubtitleData data) {
notifySessionPlayerCallback(new SessionPlayerCallbackNotifier() {
@Override
public void callCallback(SessionPlayer.PlayerCallback callback) {
callback.onSubtitleData(MediaPlayer.this, item, track, data);
}
});
}
@Override
public void onTracksChanged(@NonNull MediaPlayer2 mp,
@NonNull List<SessionPlayer.TrackInfo> tracks) {
notifySessionPlayerCallback(callback -> callback.onTracksChanged(MediaPlayer.this,
tracks));
}
}
/**
* Interface definition for callbacks to be invoked when the player has the corresponding
* events.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
public abstract static class PlayerCallback extends SessionPlayer.PlayerCallback {
/**
* @deprecated Use
* {@link #onVideoSizeChanged(SessionPlayer,androidx.media2.common.VideoSize)} instead.
*/
@Deprecated
public void onVideoSizeChanged(
@NonNull MediaPlayer mp, @NonNull MediaItem item, @NonNull VideoSize size) { }
/**
* 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 player the player associated with this callback
* @param size the size of the video
*/
@Override
public void onVideoSizeChanged(@NonNull SessionPlayer player,
@NonNull androidx.media2.common.VideoSize size) {
if (!(player instanceof MediaPlayer)) {
throw new IllegalArgumentException("player must be MediaPlayer");
}
onVideoSizeChanged((MediaPlayer) player, player.getCurrentMediaItem(),
new VideoSize(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
*/
@RestrictTo(LIBRARY)
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 #getTracks
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
public static final class TrackInfo extends SessionPlayer.TrackInfo {
TrackInfo(SessionPlayer.TrackInfo infoInternal) {
super(infoInternal.getId(), infoInternal.getTrackType(), infoInternal.getFormat(),
(infoInternal.getTrackType() != MEDIA_TRACK_TYPE_VIDEO));
}
@Nullable
@Override
public MediaFormat getFormat() {
if (getTrackType() == MEDIA_TRACK_TYPE_SUBTITLE) {
return super.getFormat();
}
return null;
}
}
/**
* Encapsulates the DRM properties of the source.
*/
@RestrictTo(LIBRARY)
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}.
*/
@RestrictTo(LIBRARY)
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
*/
@RestrictTo(LIBRARY)
public static class NoDrmSchemeException extends Exception {
public NoDrmSchemeException(@Nullable String detailMessage) {
super(detailMessage);
}
}
/**
* Definitions for the metrics that are reported via the {@link #getMetrics} call.
*/
@RestrictTo(LIBRARY)
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.
*/
@RestrictTo(LIBRARY)
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;
@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)
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;
}
}
}