public final class

MediaPlayer2Impl

extends MediaPlayer2

 java.lang.Object

androidx.media2.MediaPlayer2

↳androidx.media2.MediaPlayer2Impl

Gradle dependencies

compile group: 'androidx.media2', name: 'media2', version: '1.0.0-alpha04'

  • groupId: androidx.media2
  • artifactId: media2
  • version: 1.0.0-alpha04

Artifact androidx.media2:media2:1.0.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.media2:media2 com.android.support:media2

Overview

MediaPlayer2 implementation for platform version P (28).

Summary

Fields
from MediaPlayer2CALL_COMPLETED_ATTACH_AUX_EFFECT, CALL_COMPLETED_DESELECT_TRACK, CALL_COMPLETED_LOOP_CURRENT, CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, CALL_COMPLETED_PAUSE, CALL_COMPLETED_PLAY, CALL_COMPLETED_PREPARE, CALL_COMPLETED_PREPARE_DRM, CALL_COMPLETED_SEEK_TO, CALL_COMPLETED_SELECT_TRACK, CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, CALL_COMPLETED_SET_AUDIO_SESSION_ID, CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, CALL_COMPLETED_SET_DATA_SOURCE, CALL_COMPLETED_SET_NEXT_DATA_SOURCE, CALL_COMPLETED_SET_NEXT_DATA_SOURCES, CALL_COMPLETED_SET_PLAYBACK_PARAMS, CALL_COMPLETED_SET_PLAYER_VOLUME, CALL_COMPLETED_SET_SURFACE, CALL_COMPLETED_SKIP_TO_NEXT, CALL_STATUS_BAD_VALUE, CALL_STATUS_ERROR_IO, CALL_STATUS_ERROR_UNKNOWN, CALL_STATUS_INVALID_OPERATION, CALL_STATUS_NO_ERROR, CALL_STATUS_PERMISSION_DENIED, CALL_STATUS_SKIPPED, MEDIA_ERROR_IO, MEDIA_ERROR_MALFORMED, MEDIA_ERROR_SYSTEM, MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_RENDERING_START, MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_UPDATE, MEDIA_INFO_DATA_SOURCE_END, MEDIA_INFO_DATA_SOURCE_LIST_END, MEDIA_INFO_DATA_SOURCE_REPEAT, MEDIA_INFO_DATA_SOURCE_START, MEDIA_INFO_EXTERNAL_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_NETWORK_BANDWIDTH, MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_PREPARED, MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_TIMED_TEXT_ERROR, MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_TRACK_LAGGING, PLAYER_STATE_ERROR, PLAYER_STATE_IDLE, PLAYER_STATE_PAUSED, PLAYER_STATE_PLAYING, PLAYER_STATE_PREPARED, PREPARE_DRM_STATUS_PREPARATION_ERROR, PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, PREPARE_DRM_STATUS_RESOURCE_BUSY, PREPARE_DRM_STATUS_SUCCESS, PREPARE_DRM_STATUS_UNSUPPORTED_SCHEME, SEEK_CLOSEST, SEEK_CLOSEST_SYNC, SEEK_NEXT_SYNC, SEEK_PREVIOUS_SYNC, SEPARATE_CALL_COMPLETE_CALLBACK_START, VIDEO_SCALING_MODE_SCALE_TO_FIT
Constructors
publicMediaPlayer2Impl()

Default constructor.

Methods
public abstract java.lang.ObjectattachAuxEffect(int effectId)

Attaches an auxiliary effect to the player.

public abstract booleancancel(java.lang.Object token)

Cancels the asynchronous call previously submitted.

public abstract voidclearDrmEventCallback()

Clears the MediaPlayer2.DrmEventCallback.

public abstract voidclearEventCallback()

Clears the MediaPlayer2.EventCallback.

public abstract voidclearPendingCommands()

Discards all pending commands.

public voidclose()

Releases the resources held by this MediaPlayer2 object.

public abstract java.lang.ObjectdeselectTrack(int index)

Deselects a track.

public abstract AudioAttributesCompatgetAudioAttributes()

Gets the audio attributes for this MediaPlayer2.

public abstract intgetAudioSessionId()

Returns the audio session ID.

public abstract longgetBufferedPosition()

Gets the current buffered media source position received through progressive downloading.

public abstract MediaItemgetCurrentMediaItem()

Gets the current media item as described by a MediaItem.

public abstract longgetCurrentPosition()

Gets the current playback position.

public abstract MediaPlayer2.DrmInfogetDrmInfo()

Retrieves the DRM Info associated with the current source

public abstract MediaDrm.KeyRequestgetDrmKeyRequest(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 abstract java.lang.StringgetDrmPropertyString(java.lang.String propertyName)

Read a DRM engine plugin String property value, given the property name string.

public abstract longgetDuration()

Gets the duration of the file.

public floatgetMaxPlayerVolume()

public abstract PersistableBundlegetMetrics()

Return Metrics data about the current player.

public abstract PlaybackParamsgetPlaybackParams()

Gets the playback params, containing the current playback rate.

public abstract floatgetPlayerVolume()

Returns the current volume of this player to this player.

public abstract intgetSelectedTrack(int trackType)

Returns the index of the audio, video, or subtitle track currently selected for playback, The return value is an index into the array returned by MediaPlayer2.getTrackInfo(), and can be used in calls to MediaPlayer2.selectTrack(int) or MediaPlayer2.deselectTrack(int).

public abstract intgetState()

Gets the current MediaPlayer2 state.

public abstract MediaTimestampgetTimestamp()

Gets current playback position as a MediaTimestamp.

public abstract java.util.List<MediaPlayer2.TrackInfo>getTrackInfo()

Returns a List of track information.

public abstract intgetVideoHeight()

Returns the height of the video.

public abstract intgetVideoWidth()

Returns the width of the video.

public abstract java.lang.ObjectloopCurrent(boolean loop)

Configures the player to loop on the current media item.

public abstract java.lang.ObjectnotifyWhenCommandLabelReached(java.lang.Object label)

Insert a task in the command queue to help the client to identify whether a batch of commands has been finished.

public abstract java.lang.Objectpause()

Pauses playback.

public abstract java.lang.Objectplay()

Starts or resumes playback.

public abstract java.lang.Objectprepare()

Prepares the player for playback, asynchronously.

public abstract java.lang.ObjectprepareDrm(java.util.UUID uuid)

Prepares the DRM for the current source

public abstract 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 abstract voidreleaseDrm()

Releases the DRM session

public voidreset()

Resets the MediaPlayer2 to its uninitialized state.

public abstract voidrestoreDrmKeys(byte[] keySetId[])

Restore persisted offline keys into a new session.

public abstract java.lang.ObjectseekTo(long msec, int mode)

Moves the media to specified time position by considering the given mode.

public abstract java.lang.ObjectselectTrack(int index)

Selects a track.

public abstract java.lang.ObjectsetAudioAttributes(AudioAttributesCompat attributes)

Sets the audio attributes for this MediaPlayer2.

public abstract java.lang.ObjectsetAudioSessionId(int sessionId)

Sets the audio session ID.

public abstract java.lang.ObjectsetAuxEffectSendLevel(float level)

Sets the send level of the player to the attached auxiliary effect.

public abstract voidsetDrmEventCallback(java.util.concurrent.Executor executor, MediaPlayer2.DrmEventCallback eventCallback)

Sets the callback to be invoked when the media source is ready for playback.

public abstract voidsetDrmPropertyString(java.lang.String propertyName, java.lang.String value)

Set a DRM engine plugin String property value.

public abstract voidsetEventCallback(java.util.concurrent.Executor executor, MediaPlayer2.EventCallback eventCallback)

Sets the callback to be invoked when the media source is ready for playback.

public abstract java.lang.ObjectsetMediaItem(MediaItem item)

Sets the media item as described by a MediaItem.

public abstract java.lang.ObjectsetNextMediaItem(MediaItem item)

Sets a single media item as described by a MediaItem which will be played after current media item is finished.

public abstract java.lang.ObjectsetNextMediaItems(java.util.List<MediaItem> items)

Sets a list of media items to be played sequentially after current media item is done.

public abstract voidsetOnDrmConfigHelper(MediaPlayer2.OnDrmConfigHelper listener)

Register a callback to be invoked for configuration of the DRM object before the session is created.

public abstract java.lang.ObjectsetPlaybackParams(PlaybackParams params)

Sets playback rate using PlaybackParams.

public abstract java.lang.ObjectsetPlayerVolume(float volume)

Sets the volume of the audio of the media to play, expressed as a linear multiplier on the audio samples.

public abstract java.lang.ObjectsetSurface(Surface surface)

Sets the to be used as the sink for the video portion of the media.

public abstract java.lang.ObjectskipToNext()

Tries to play next media item if applicable.

from MediaPlayer2create, seekTo
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public MediaPlayer2Impl()

Default constructor.

When done with the MediaPlayer2Impl, you should call MediaPlayer2Impl.close(), to free the resources. If not released, too many MediaPlayer2Impl instances may result in an exception.

Methods

public abstract boolean cancel(java.lang.Object token)

Cancels the asynchronous call previously submitted.

Parameters:

token: the token which is returned from the asynchronous call.

Returns:

false if the task could not be cancelled; true otherwise.

public void close()

Releases the resources held by this MediaPlayer2 object. It is considered good practice to call this method when you're done using the MediaPlayer2. In particular, whenever an Activity of an application is paused (its onPause() method is called), or stopped (its onStop() method is called), this method should be invoked to release the MediaPlayer2 object, unless the application has a special need to keep the object around. In addition to unnecessary resources (such as memory and instances of codecs) being held, failure to call this method immediately if a MediaPlayer2 object is no longer needed may also lead to continuous battery consumption for mobile devices, and playback failure for other applications if no multiple instances of the same codec are supported on a device. Even if multiple instances of the same codec are supported, some performance degradation may be expected when unnecessary multiple instances are used at the same time. close() may be safely called after a prior close(). This class implements the Java AutoCloseable interface and may be used with try-with-resources.

public abstract java.lang.Object play()

Starts or resumes playback. If playback had previously been paused, playback will continue from where it was paused. If playback had reached end of stream and been paused, or never started before, playback will start at the beginning. If the source had not been prepared, the player will prepare the source and play.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object prepare()

Prepares the player for playback, asynchronously. After setting the datasource and the display surface, you need to call prepare().

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object pause()

Pauses playback. Call play() to resume.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object skipToNext()

Tries to play next media item if applicable.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract long getCurrentPosition()

Gets the current playback position.

Returns:

the current position in milliseconds

public abstract long getDuration()

Gets the duration of the file.

Returns:

the duration in milliseconds, if no duration is available (for example, if streaming live content), -1 is returned.

public abstract long getBufferedPosition()

Gets the current buffered media source position 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.

Returns:

the current buffered media source position in milliseconds

public abstract int getState()

Gets the current MediaPlayer2 state.

Returns:

the current MediaPlayer2 state.

public abstract java.lang.Object setAudioAttributes(AudioAttributesCompat attributes)

Sets the audio attributes for this MediaPlayer2. See AudioAttributesCompat for how to build and configure an instance of this class. You must call this method before MediaPlayer2.prepare() in order for the audio attributes to become effective thereafter.

Parameters:

attributes: a non-null set of audio attributes

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract AudioAttributesCompat getAudioAttributes()

Gets the audio attributes for this MediaPlayer2.

Returns:

attributes a set of audio attributes

public abstract java.lang.Object setMediaItem(MediaItem item)

Sets the media item as described by a MediaItem.

Parameters:

item: the descriptor of media item you want to play

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object setNextMediaItem(MediaItem item)

Sets a single media item as described by a MediaItem which will be played after current media item is finished.

Parameters:

item: the descriptor of media item you want to play after current one

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object setNextMediaItems(java.util.List<MediaItem> items)

Sets a list of media items to be played sequentially after current media item is done.

Parameters:

items: the list of media items you want to play after current one

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract MediaItem getCurrentMediaItem()

Gets the current media item as described by a MediaItem.

Returns:

the current MediaItem

public abstract java.lang.Object loopCurrent(boolean loop)

Configures the player to loop on the current media item.

Parameters:

loop: true if the current media item is meant to loop.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object 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 MediaPlayer2.getMaxPlayerVolume() for the volume range supported by this player.

Parameters:

volume: a value between 0.0f and MediaPlayer2.getMaxPlayerVolume().

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract float getPlayerVolume()

Returns the current volume of this player to this player. Note that it does not take into account the associated stream volume.

Returns:

the player volume.

public float getMaxPlayerVolume()

Returns:

the maximum volume that can be used in MediaPlayer2.setPlayerVolume(float).

public abstract java.lang.Object notifyWhenCommandLabelReached(java.lang.Object label)

Insert a task in the command queue to help the client to identify whether a batch of commands has been finished. When this command is processed, a notification MediaPlayer2.EventCallback.onCommandLabelReached(MediaPlayer2, Object) will be fired with the given label.

Parameters:

label: An application specific Object used to help to identify the completeness of a batch of commands.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

See also: MediaPlayer2.EventCallback.onCommandLabelReached(MediaPlayer2, Object)

public abstract java.lang.Object setSurface(Surface surface)

Sets the to be used as the sink for the video portion of the media. Setting a Surface will un-set any Surface or SurfaceHolder that was previously set. 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.

Parameters:

surface: The to be used for the video portion of the media.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract void clearPendingCommands()

Discards all pending commands.

public abstract int getVideoWidth()

Returns the width of the video.

Returns:

the width of the video, or 0 if there is no video, no display surface was set, or the width has not been determined yet. The MediaPlayer2.EventCallback can be registered via MediaPlayer2.setEventCallback(Executor, MediaPlayer2.EventCallback) to provide a notification MediaPlayer2.EventCallback.onVideoSizeChanged(MediaPlayer2, MediaItem, int, int) when the width is available.

public abstract int getVideoHeight()

Returns the height of the video.

Returns:

the height of the video, or 0 if there is no video, no display surface was set, or the height has not been determined yet. The MediaPlayer2.EventCallback can be registered via MediaPlayer2.setEventCallback(Executor, MediaPlayer2.EventCallback) to provide a notification MediaPlayer2.EventCallback.onVideoSizeChanged(MediaPlayer2, MediaItem, int, int) when the height is available.

public abstract PersistableBundle getMetrics()

Return Metrics data about the current player.

Returns:

a containing the set of attributes and values available for the media being handled by this instance of MediaPlayer2 The attributes are descibed in MediaPlayer2.MetricsConstants. Additional vendor-specific fields may also be present in the return value.

public abstract java.lang.Object setPlaybackParams(PlaybackParams params)

Sets playback rate using PlaybackParams. The player sets its internal PlaybackParams to the given input. This does not change the player state. For example, if this is called with the speed of 2.0f in MediaPlayer2.PLAYER_STATE_PAUSED, the player will just update internal property and stay paused. Once the client calls MediaPlayer2.play() afterwards, the player will start playback with the given speed. Calling this with zero speed is not allowed.

Parameters:

params: the playback params.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract PlaybackParams getPlaybackParams()

Gets the playback params, containing the current playback rate.

Returns:

the playback params.

public abstract java.lang.Object seekTo(long msec, int mode)

Moves the media to specified time position by considering the given mode.

When seekTo is finished, the user will be notified via MediaPlayer2.EventCallback.onInfo(MediaPlayer2, MediaItem, int, int) with MediaPlayer2.CALL_COMPLETED_SEEK_TO. 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.

Parameters:

msec: 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. If msec is negative, time position zero will be used. If msec is larger than duration, duration will be used.
mode: the mode indicating where exactly to seek to.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract MediaTimestamp getTimestamp()

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 void reset()

Resets the MediaPlayer2 to its uninitialized state. After calling this method, you will have to initialize it again by setting the media item and calling prepare().

public abstract java.lang.Object 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 one of the overloaded setMediaItem methods.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract int getAudioSessionId()

Returns the audio session ID.

Returns:

the audio session ID. MediaPlayer2.setAudioSessionId(int) Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed.

public abstract java.lang.Object 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 MediaPlayer2.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 after one of the overloaded setMediaItem methods.

Parameters:

effectId: system wide unique id of the effect to attach

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.lang.Object setAuxEffectSendLevel(float level)

Sets the send level of the player to the attached auxiliary effect. See MediaPlayer2.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)

Parameters:

level: send level scalar

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract java.util.List<MediaPlayer2.TrackInfo> getTrackInfo()

Returns a List of track information.

Returns:

List of track info. The total number of tracks is the array length. Must be called again if an external timed text source has been added after addTimedTextSource method is called.

public abstract int getSelectedTrack(int trackType)

Returns the index of the audio, video, or subtitle track currently selected for playback, The return value is an index into the array returned by MediaPlayer2.getTrackInfo(), and can be used in calls to MediaPlayer2.selectTrack(int) or MediaPlayer2.deselectTrack(int).

Parameters:

trackType: should be one of MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_VIDEO, MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_AUDIO, or MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE

Returns:

index of the audio, video, or subtitle track currently selected for playback; a negative integer is returned when there is no selected track for trackType or when trackType is not one of audio, video, or subtitle.

See also: MediaPlayer2.getTrackInfo(), MediaPlayer2.selectTrack(int), MediaPlayer2.deselectTrack(int)

public abstract java.lang.Object selectTrack(int index)

Selects a track.

If a MediaPlayer2 is in invalid state, MediaPlayer2.CALL_STATUS_INVALID_OPERATION will be reported with MediaPlayer2.EventCallback.onCallCompleted(MediaPlayer2, MediaItem, int, int). If a MediaPlayer2 is in Playing state, the selected track is presented immediately. If a MediaPlayer2 is not in Started 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, Timed Text), 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. However, no timed text track will be selected until this function is called.

Currently, only timed text tracks or audio tracks can be selected via this method.

Parameters:

index: the index of the track to be selected. The valid range of the index is 0..total number of track - 1. The total number of tracks as well as the type of each individual track can be found by calling MediaPlayer2.getTrackInfo() method.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

See also: MediaPlayer2.getTrackInfo()

public abstract java.lang.Object deselectTrack(int index)

Deselects a track.

Currently, the track must be a timed text track and no audio or video tracks can be deselected. If the timed text track identified by index has not been selected before, it throws an exception.

Parameters:

index: the index of the track to be deselected. The valid range of the index is 0..total number of tracks - 1. The total number of tracks as well as the type of each individual track can be found by calling MediaPlayer2.getTrackInfo() method.

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

See also: MediaPlayer2.getTrackInfo()

public abstract void setEventCallback(java.util.concurrent.Executor executor, MediaPlayer2.EventCallback eventCallback)

Sets the callback to be invoked when the media source is ready for playback.

Parameters:

eventCallback: the callback that will be run
executor: the executor through which the callback should be invoked

public abstract void clearEventCallback()

Clears the MediaPlayer2.EventCallback.

public abstract void setOnDrmConfigHelper(MediaPlayer2.OnDrmConfigHelper listener)

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 MediaPlayer2.prepareDrm(UUID).

Parameters:

listener: the callback that will be run

public abstract void setDrmEventCallback(java.util.concurrent.Executor executor, MediaPlayer2.DrmEventCallback eventCallback)

Sets the callback to be invoked when the media source is ready for playback.

Parameters:

eventCallback: the callback that will be run
executor: the executor through which the callback should be invoked

public abstract void clearDrmEventCallback()

Clears the MediaPlayer2.DrmEventCallback.

public abstract MediaPlayer2.DrmInfo getDrmInfo()

Retrieves the DRM Info associated with the current source

public abstract java.lang.Object prepareDrm(java.util.UUID uuid)

Prepares the DRM for the current source

If MediaPlayer2.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 MediaPlayer2.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. MediaPlayer2.DrmEventCallback.onDrmPrepared(MediaPlayer2, MediaItem, int) will be called when provisioning and preparation has finished. The application should check the status code returned with MediaPlayer2.DrmEventCallback.onDrmPrepared(MediaPlayer2, MediaItem, int) to proceed.

Parameters:

uuid: The UUID of the crypto scheme. If not known beforehand, it can be retrieved from the source through MediaPlayer2.getDrmInfo() or registering MediaPlayer2.DrmEventCallback.onDrmInfo(MediaPlayer2, MediaItem, MediaPlayer2.DrmInfo).

Returns:

a token which can be used to cancel the operation later with MediaPlayer2.cancel(Object).

public abstract void releaseDrm()

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 abstract 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 MediaPlayer2.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 abstract 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 MediaPlayer2.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 MediaPlayer2.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 abstract void restoreDrmKeys(byte[] keySetId[])

Restore persisted offline keys into a new session. keySetId identifies the keys to load, obtained from a prior call to MediaPlayer2.provideDrmKeyResponse(byte[], byte[]).

Parameters:

keySetId: identifies the saved key set to restore

public abstract 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 abstract 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

Source

/*
 * Copyright 2018 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;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;

import android.annotation.TargetApi;
import android.media.AudioAttributes;
import android.media.DeniedByServerException;
import android.media.MediaDataSource;
import android.media.MediaDrm;
import android.media.MediaFormat;
import android.media.MediaPlayer;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.Surface;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import androidx.media.AudioAttributesCompat;
import androidx.media2.SessionPlayer.BuffState;
import androidx.media2.SessionPlayer.PlayerState;
import androidx.media2.common.TrackInfoImpl;

import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * MediaPlayer2 implementation for platform version P (28).
 * @hide
 */
@TargetApi(Build.VERSION_CODES.P)
@RestrictTo(LIBRARY_GROUP)
public final class MediaPlayer2Impl extends MediaPlayer2 {

    private static final String TAG = "MediaPlayer2Impl";

    private static final int SOURCE_STATE_ERROR = -1;
    private static final int SOURCE_STATE_INIT = 0;
    private static final int SOURCE_STATE_PREPARING = 1;
    private static final int SOURCE_STATE_PREPARED = 2;

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static final android.media.PlaybackParams DEFAULT_PLAYBACK_PARAMS =
            new android.media.PlaybackParams().allowDefaults();

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static ArrayMap<Integer, Integer> sInfoEventMap;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static ArrayMap<Integer, Integer> sErrorEventExtraMap;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static ArrayMap<Integer, Integer> sStateMap;
    static ArrayMap<Integer, Integer> sTrackTypeMap;

    static {
        sInfoEventMap = new ArrayMap<>();
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT, MEDIA_INFO_UNKNOWN);
        sInfoEventMap.put(
                MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START);
        sInfoEventMap.put(
                MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_START);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_END);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING);
        sInfoEventMap.put(
                MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_UNSUPPORTED_SUBTITLE);
        sInfoEventMap.put(MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_SUBTITLE_TIMED_OUT);

        sErrorEventExtraMap = new ArrayMap<>();
        sErrorEventExtraMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
        sErrorEventExtraMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
        sErrorEventExtraMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
        sErrorEventExtraMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);

        sStateMap = new ArrayMap<>();
        sStateMap.put(PLAYER_STATE_IDLE, SessionPlayer.PLAYER_STATE_IDLE);
        sStateMap.put(PLAYER_STATE_PREPARED, SessionPlayer.PLAYER_STATE_PAUSED);
        sStateMap.put(PLAYER_STATE_PAUSED, SessionPlayer.PLAYER_STATE_PAUSED);
        sStateMap.put(PLAYER_STATE_PLAYING, SessionPlayer.PLAYER_STATE_PLAYING);
        sStateMap.put(PLAYER_STATE_ERROR, SessionPlayer.PLAYER_STATE_ERROR);

        sTrackTypeMap = new ArrayMap<>();
        sTrackTypeMap.put(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN,
                TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN);
        sTrackTypeMap.put(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO,
                TrackInfo.MEDIA_TRACK_TYPE_VIDEO);
        sTrackTypeMap.put(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO,
                TrackInfo.MEDIA_TRACK_TYPE_AUDIO);
        sTrackTypeMap.put(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT,
                TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN);
        sTrackTypeMap.put(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE,
                TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE);
        sTrackTypeMap.put(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA,
                TrackInfo.MEDIA_TRACK_TYPE_METADATA);
    }

    MediaPlayerSourceQueue mPlayer;

    private HandlerThread mHandlerThread;
    private final Handler mEndPositionHandler;
    private final Handler mTaskHandler;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mTaskLock = new Object();
    @GuardedBy("mTaskLock")
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final ArrayDeque<Task> mPendingTasks = new ArrayDeque<>();
    @GuardedBy("mTaskLock")
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    Task mCurrentTask;

    private final Object mLock = new Object();
    //--- guarded by |mLock| start
    private Pair<Executor, EventCallback> mMp2EventCallbackRecord;
    private Pair<Executor, DrmEventCallback> mDrmEventCallbackRecord;
    //--- guarded by |mLock| end

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void handleDataSourceError(final DataSourceError err) {
        if (err == null) {
            return;
        }
        notifyMediaPlayer2Event(new Mp2EventNotifier() {
            @Override
            public void notify(EventCallback callback) {
                callback.onError(MediaPlayer2Impl.this, err.mDSD, err.mWhat, err.mExtra);
            }
        });
    }

    /**
     * Default constructor.
     * <p>When done with the MediaPlayer2Impl, you should call  {@link #close()},
     * to free the resources. If not released, too many MediaPlayer2Impl instances may
     * result in an exception.</p>
     */
    public MediaPlayer2Impl() {
        mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
        mHandlerThread.start();
        Looper looper = mHandlerThread.getLooper();
        mEndPositionHandler = new Handler(looper);
        mTaskHandler = new Handler(looper);

        // TODO: To make sure MediaPlayer1 listeners work, the caller thread should have a looper.
        // Fix the framework or document this behavior.
        mPlayer = new MediaPlayerSourceQueue();
    }

    @Override
    public boolean cancel(Object token) {
        synchronized (mTaskLock) {
            return mPendingTasks.remove(token);
        }
    }

    /**
     * Releases the resources held by this {@code MediaPlayer2} object.
     *
     * It is considered good practice to call this method when you're
     * done using the MediaPlayer2. In particular, whenever an Activity
     * of an application is paused (its onPause() method is called),
     * or stopped (its onStop() method is called), this method should be
     * invoked to release the MediaPlayer2 object, unless the application
     * has a special need to keep the object around. In addition to
     * unnecessary resources (such as memory and instances of codecs)
     * being held, failure to call this method immediately if a
     * MediaPlayer2 object is no longer needed may also lead to
     * continuous battery consumption for mobile devices, and playback
     * failure for other applications if no multiple instances of the
     * same codec are supported on a device. Even if multiple instances
     * of the same codec are supported, some performance degradation
     * may be expected when unnecessary multiple instances are used
     * at the same time.
     *
     * {@code close()} may be safely called after a prior {@code close()}.
     * This class implements the Java {@code AutoCloseable} interface and
     * may be used with try-with-resources.
     */
    @Override
    public void close() {
        clearEventCallback();
        clearDrmEventCallback();
        mPlayer.release();
        if (mHandlerThread != null) {
            mHandlerThread.quitSafely();
            mHandlerThread = null;
        }
    }

    @Override
    public Object play() {
        return addTask(new Task(CALL_COMPLETED_PLAY, false) {
            @Override
            void process() {
                mPlayer.play();
            }
        });
    }

    @Override
    public Object prepare() {
        return addTask(new Task(CALL_COMPLETED_PREPARE, true) {
            @Override
            void process() throws IOException {
                mPlayer.prepareAsync();
            }
        });
    }

    @Override
    public Object pause() {
        return addTask(new Task(CALL_COMPLETED_PAUSE, false) {
            @Override
            void process() {
                mPlayer.pause();
            }
        });
    }

    @Override
    public Object skipToNext() {
        return addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
            @Override
            void process() {
                mPlayer.skipToNext();
            }
        });
    }

    @Override
    public long getCurrentPosition() {
        return mPlayer.getCurrentPosition();
    }

    @Override
    public long getDuration() {
        return mPlayer.getDuration();
    }

    @Override
    public long getBufferedPosition() {
        // Use cached buffered percent for now.
        return mPlayer.getBufferedPosition();
    }

    @Override
    public @MediaPlayer2State int getState() {
        return mPlayer.getMediaPlayer2State();
    }

    @SessionPlayer.PlayerState int getPlayerState() {
        return mPlayer.getPlayerState();
    }

    /**
     * Gets the current buffering state of the player.
     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
     * buffered.
     */
    @SessionPlayer.BuffState int getBufferingState() {
        return  mPlayer.getBufferingState();
    }

    @Override
    public Object setAudioAttributes(@NonNull final AudioAttributesCompat attributes) {
        return addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
            @Override
            void process() {
                mPlayer.setAudioAttributes(attributes);
            }
        });
    }

    @Override
    public @Nullable AudioAttributesCompat getAudioAttributes() {
        return mPlayer.getAudioAttributes();
    }

    @Override
    public Object setMediaItem(@NonNull final MediaItem item) {
        return addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
            @Override
            void process() {
                Preconditions.checkArgument(item != null, "the MediaItem cannot be null");
                // TODO: setMediaItem could update exist media item
                try {
                    mPlayer.setFirst(item);
                } catch (IOException e) {
                    Log.e(TAG, "process: setMediaItem", e);
                }
            }
        });
    }

    @Override
    public Object setNextMediaItem(@NonNull final MediaItem item) {
        return addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
            @Override
            void process() {
                Preconditions.checkArgument(item != null, "the MediaItem cannot be null");
                handleDataSourceError(mPlayer.setNext(item));
            }
        });
    }

    @Override
    public Object setNextMediaItems(@NonNull final List<MediaItem> items) {
        return addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
            @Override
            void process() {
                if (items == null || items.size() == 0) {
                    throw new IllegalArgumentException("media item list cannot be null or empty.");
                }
                for (MediaItem item : items) {
                    if (item == null) {
                        throw new IllegalArgumentException(
                                "MediaItem in the source list cannot be null.");
                    }
                }
                handleDataSourceError(mPlayer.setNextMultiple(items));
            }
        });
    }

    @Override
    public @Nullable MediaItem getCurrentMediaItem() {
        return mPlayer.getFirst().getDSD();
    }

    @Override
    public Object loopCurrent(final boolean loop) {
        return addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
            @Override
            void process() {
                mPlayer.setLooping(loop);
            }
        });
    }

    @Override
    public Object setPlayerVolume(final float volume) {
        return addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
            @Override
            void process() {
                mPlayer.setVolume(volume);
            }
        });
    }

    @Override
    public float getPlayerVolume() {
        return mPlayer.getVolume();
    }

    @Override
    public float getMaxPlayerVolume() {
        return 1.0f;
    }

    @Override
    public Object notifyWhenCommandLabelReached(final Object label) {
        return addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
            @Override
            void process() {
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
                    }
                });
            }
        });
    }

    @Override
    public Object setSurface(final Surface surface) {
        return addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
            @Override
            void process() {
                mPlayer.setSurface(surface);
            }
        });
    }

    @Override
    public void clearPendingCommands() {
        synchronized (mTaskLock) {
            mPendingTasks.clear();
        }
    }

    private Object addTask(Task task) {
        synchronized (mTaskLock) {
            mPendingTasks.add(task);
            processPendingTask_l();
        }
        return task;
    }

    @GuardedBy("mTaskLock")
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void processPendingTask_l() {
        if (mCurrentTask != null) {
            return;
        }
        if (!mPendingTasks.isEmpty()) {
            Task task = mPendingTasks.removeFirst();
            mCurrentTask = task;
            mTaskHandler.post(task);
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static void handleDataSource(MediaPlayerSource src)
            throws IOException {
        final MediaItem item = src.getDSD();
        Preconditions.checkArgument(item != null, "the MediaItem cannot be null");

        MediaPlayer player = src.getPlayer();
        if (item instanceof CallbackMediaItem) {
            player.setDataSource(new MediaDataSource() {
                DataSourceCallback mDataSource =
                        ((CallbackMediaItem) item).getDataSourceCallback();

                @Override
                public int readAt(long position, byte[] buffer, int offset, int size)
                        throws IOException {
                    return mDataSource.readAt(position, buffer, offset, size);
                }

                @Override
                public long getSize() throws IOException {
                    return mDataSource.getSize();
                }

                @Override
                public void close() throws IOException {
                    mDataSource.close();
                }
            });
        } else if (item instanceof FileMediaItem) {
            FileMediaItem fitem = (FileMediaItem) item;
            player.setDataSource(
                    fitem.getFileDescriptor(),
                    fitem.getFileDescriptorOffset(),
                    fitem.getFileDescriptorLength());
        } else if (item instanceof UriMediaItem) {
            UriMediaItem uitem = (UriMediaItem) item;
            player.setDataSource(
                    uitem.getUriContext(),
                    uitem.getUri(),
                    uitem.getUriHeaders(),
                    uitem.getUriCookies());
        } else {
            throw new IllegalArgumentException(
                    "Unsupported media item description. " + item.toString());
        }
    }

    @Override
    public int getVideoWidth() {
        return mPlayer.getVideoWidth();
    }

    @Override
    public int getVideoHeight() {
        return mPlayer.getVideoHeight();
    }

    @Override
    public PersistableBundle getMetrics() {
        return mPlayer.getMetrics();
    }

    @Override
    public Object setPlaybackParams(@NonNull final PlaybackParams params) {
        return addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
            @Override
            void process() {
                mPlayer.setPlaybackParams(params.getPlaybackParams());
            }
        });
    }

    @Override
    @NonNull
    public PlaybackParams getPlaybackParams() {
        return new PlaybackParams.Builder(mPlayer.getPlaybackParams()).build();
    }

    @Override
    public Object seekTo(final long msec, @SeekMode final int mode) {
        return addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
            @Override
            void process() {
                mPlayer.seekTo(msec, mode);
            }
        });
    }

    @Override
    @Nullable
    public MediaTimestamp getTimestamp() {
        return mPlayer.getTimestamp();
    }

    /**
     * Resets the MediaPlayer2 to its uninitialized state. After calling
     * this method, you will have to initialize it again by setting the
     * media item and calling prepare().
     */
    @Override
    public void reset() {
        clearPendingCommands();

        // Make sure that the current task finishes.
        Task currentTask;
        synchronized (mTaskLock) {
            currentTask = mCurrentTask;
        }
        if (currentTask != null) {
            synchronized (currentTask) {
                try {
                    while (!currentTask.mDone) {
                        currentTask.wait();
                    }
                } catch (InterruptedException e) {
                }
            }
        }
        mEndPositionHandler.removeCallbacksAndMessages(null);
        mTaskHandler.removeCallbacksAndMessages(null);
        mPlayer.reset();
    }

    @Override
    public Object setAudioSessionId(final int sessionId) {
        return addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
            @Override
            void process() {
                mPlayer.setAudioSessionId(sessionId);
            }
        });
    }

    @Override
    public int getAudioSessionId() {
        return mPlayer.getAudioSessionId();
    }

    @Override
    public Object attachAuxEffect(final int effectId) {
        return addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
            @Override
            void process() {
                mPlayer.attachAuxEffect(effectId);
            }
        });
    }

    @Override
    public Object setAuxEffectSendLevel(final float level) {
        return addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
            @Override
            void process() {
                mPlayer.setAuxEffectSendLevel(level);
            }
        });
    }

    @Override
    public List<TrackInfo> getTrackInfo() {
        MediaPlayer.TrackInfo[] list = mPlayer.getTrackInfo();
        List<TrackInfo> trackList = new ArrayList<>();
        for (MediaPlayer.TrackInfo info : list) {
            int trackType = sTrackTypeMap.get(info.getTrackType());
            MediaFormat format = info.getFormat();
            if (format != null && TextUtils.equals(
                    format.getString(MediaFormat.KEY_MIME), MediaFormat.MIMETYPE_TEXT_VTT)) {
                // Hide WebVTT track. For background, see b/120081663.
                trackType = TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN;
            }
            trackList.add(new TrackInfoImpl(trackType, format));
        }
        return trackList;
    }

    @Override
    public int getSelectedTrack(int trackType) {
        return mPlayer.getSelectedTrack(trackType);
    }

    @Override
    public Object selectTrack(final int index) {
        return addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
            @Override
            void process() {
                mPlayer.selectTrack(index);
            }
        });
    }

    @Override
    public Object deselectTrack(final int index) {
        return addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
            @Override
            void process() {
                mPlayer.deselectTrack(index);
            }
        });
    }

    @Override
    public void setEventCallback(@NonNull Executor executor,
            @NonNull EventCallback eventCallback) {
        if (eventCallback == null) {
            throw new IllegalArgumentException("Illegal null EventCallback");
        }
        if (executor == null) {
            throw new IllegalArgumentException(
                    "Illegal null Executor for the EventCallback");
        }
        synchronized (mLock) {
            mMp2EventCallbackRecord = new Pair(executor, eventCallback);
        }
    }

    @Override
    public void clearEventCallback() {
        synchronized (mLock) {
            mMp2EventCallbackRecord = null;
        }
    }

    // Modular DRM begin

    @Override
    public void setOnDrmConfigHelper(final OnDrmConfigHelper listener) {
        mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
            @Override
            public void onDrmConfig(MediaPlayer mp) {
                MediaPlayerSource src = mPlayer.getSourceForPlayer(mp);
                MediaItem item = src == null ? null : src.getDSD();
                listener.onDrmConfig(MediaPlayer2Impl.this, item);
            }
        });
    }

    @Override
    public void setDrmEventCallback(@NonNull Executor executor,
                                    @NonNull DrmEventCallback eventCallback) {
        if (eventCallback == null) {
            throw new IllegalArgumentException("Illegal null EventCallback");
        }
        if (executor == null) {
            throw new IllegalArgumentException(
                    "Illegal null Executor for the EventCallback");
        }
        synchronized (mLock) {
            mDrmEventCallbackRecord = new Pair(executor, eventCallback);
        }
    }

    @Override
    public void clearDrmEventCallback() {
        synchronized (mLock) {
            mDrmEventCallbackRecord = null;
        }
    }


    @Override
    public DrmInfo getDrmInfo() {
        MediaPlayer.DrmInfo info = mPlayer.getDrmInfo();
        return info == null ? null : new DrmInfoImpl(info.getPssh(), info.getSupportedSchemes());
    }


    @Override
    public Object prepareDrm(@NonNull final UUID uuid) {
        return addTask(new Task(CALL_COMPLETED_PREPARE_DRM, false) {
            @Override
            void process() {
                int status = PREPARE_DRM_STATUS_SUCCESS;
                try {
                    mPlayer.prepareDrm(uuid);
                } catch (ResourceBusyException e) {
                    status = PREPARE_DRM_STATUS_RESOURCE_BUSY;
                } catch (MediaPlayer.ProvisioningServerErrorException e) {
                    status = PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR;
                } catch (MediaPlayer.ProvisioningNetworkErrorException e) {
                    status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
                } catch (UnsupportedSchemeException e) {
                    status = PREPARE_DRM_STATUS_UNSUPPORTED_SCHEME;
                } catch (Exception e) {
                    status = PREPARE_DRM_STATUS_PREPARATION_ERROR;
                }
                final int prepareDrmStatus = status;
                notifyDrmEvent(new DrmEventNotifier() {
                    @Override
                    public void notify(DrmEventCallback cb) {
                        cb.onDrmPrepared(MediaPlayer2Impl.this, mDSD, prepareDrmStatus);
                    }
                });
            }
        });
    }

    @Override
    public void releaseDrm() throws NoDrmSchemeException {
        try {
            mPlayer.releaseDrm();
        } catch (MediaPlayer.NoDrmSchemeException e) {
            throw new NoDrmSchemeException(e.getMessage());
        }
    }


    @Override
    @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.getKeyRequest(keySetId, initData, mimeType, keyType, optionalParameters);
        } catch (MediaPlayer.NoDrmSchemeException e) {
            throw new NoDrmSchemeException(e.getMessage());
        }
    }


    @Override
    public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
            throws NoDrmSchemeException, DeniedByServerException {
        try {
            return mPlayer.provideKeyResponse(keySetId, response);
        } catch (MediaPlayer.NoDrmSchemeException e) {
            throw new NoDrmSchemeException(e.getMessage());
        }
    }


    @Override
    public void restoreDrmKeys(@NonNull final byte[] keySetId)
            throws NoDrmSchemeException {
        try {
            mPlayer.restoreKeys(keySetId);
        } catch (MediaPlayer.NoDrmSchemeException e) {
            throw new NoDrmSchemeException(e.getMessage());
        }
    }


    @Override
    @NonNull
    public String getDrmPropertyString(@NonNull String propertyName)
            throws NoDrmSchemeException {
        try {
            return mPlayer.getDrmPropertyString(propertyName);
        } catch (MediaPlayer.NoDrmSchemeException e) {
            throw new NoDrmSchemeException(e.getMessage());
        }
    }


    @Override
    public void setDrmPropertyString(@NonNull String propertyName,
                                     @NonNull String value)
            throws NoDrmSchemeException {
        try {
            mPlayer.setDrmPropertyString(propertyName, value);
        } catch (MediaPlayer.NoDrmSchemeException e) {
            throw new NoDrmSchemeException(e.getMessage());
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
        final Pair<Executor, EventCallback> record;
        synchronized (mLock) {
            record = mMp2EventCallbackRecord;
        }
        if (record != null) {
            try {
                record.first.execute(new Runnable() {
                    @Override
                    public void run() {
                        notifier.notify(record.second);
                    }
                });
            } catch (RejectedExecutionException e) {
                // The given executor is shutting down.
                Log.w(TAG, "The given executor is shutting down. Ignoring the player event.");
            }
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void notifyDrmEvent(final DrmEventNotifier notifier) {
        final Pair<Executor, DrmEventCallback> record;
        synchronized (mLock) {
            record = mDrmEventCallbackRecord;
        }
        if (record != null) {
            try {
                record.first.execute(new Runnable() {
                    @Override
                    public void run() {
                        notifier.notify(record.second);
                    }
                });
            } catch (RejectedExecutionException e) {
                // The given executor is shutting down.
                Log.w(TAG, "The given executor is shutting down. Ignoring the player event.");
            }
        }
    }

    private interface Mp2EventNotifier {
        void notify(EventCallback callback);
    }

    private interface DrmEventNotifier {
        void notify(DrmEventCallback callback);
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void setEndPositionTimerIfNeeded(
            final MediaPlayer.OnCompletionListener completionListener,
            final MediaPlayerSource src, android.media.MediaTimestamp timeitem) {
        if (src == mPlayer.getFirst()) {
            mEndPositionHandler.removeCallbacksAndMessages(null);
            MediaItem item = src.getDSD();
            if (item.getEndPosition() != MediaItem.POSITION_UNKNOWN) {
                if (timeitem.getMediaClockRate() > 0.0f) {
                    long nowNs = System.nanoTime();
                    long elapsedTimeUs = (nowNs - timeitem.getAnchorSytemNanoTime()) / 1000;
                    long nowMediaMs = (timeitem.getAnchorMediaTimeUs() + elapsedTimeUs) / 1000;
                    long timeLeftMs = (long) ((item.getEndPosition() - nowMediaMs)
                            / timeitem.getMediaClockRate());
                    mEndPositionHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            if (mPlayer.getFirst() != src) {
                                return;
                            }
                            mPlayer.pause();
                            completionListener.onCompletion(src.getPlayer());
                        }
                    }, timeLeftMs < 0 ? 0 : timeLeftMs);
                }
            }
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void clearListeners(final MediaPlayerSource src) {
        MediaPlayer p = src.getPlayer();
        p.setOnPreparedListener(null);
        p.setOnVideoSizeChangedListener(null);
        p.setOnInfoListener(null);
        p.setOnCompletionListener(null);
        p.setOnErrorListener(null);
        p.setOnSeekCompleteListener(null);
        p.setOnTimedMetaDataAvailableListener(null);
        p.setOnBufferingUpdateListener(null);
        p.clearOnMediaTimeDiscontinuityListener();
        p.clearOnSubtitleDataListener();
        p.setOnDrmInfoListener(null);
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void setUpListeners(final MediaPlayerSource src) {
        MediaPlayer p = src.getPlayer();
        final MediaPlayer.OnPreparedListener preparedListener =
                new MediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(MediaPlayer mp) {
                        handleDataSourceError(mPlayer.onPrepared(mp));
                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
                            @Override
                            public void notify(EventCallback callback) {
                                MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
                                MediaItem item = src.getDSD();
                                callback.onInfo(mp2, item, MEDIA_INFO_PREPARED, 0);
                            }
                        });
                        synchronized (mTaskLock) {
                            if (mCurrentTask != null
                                    && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
                                    && ObjectsCompat.equals(mCurrentTask.mDSD, src.getDSD())
                                    && mCurrentTask.mNeedToWaitForEventToComplete) {
                                mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
                                mCurrentTask = null;
                                processPendingTask_l();
                            }
                        }
                    }
                };
        p.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                if (src.getPlayer().getDuration() >= 0) {
                    // Seeks to the start position. We call seek operation even when the start
                    // position is 0 in order to show the preview.
                    src.getPlayer().seekTo((int) src.getDSD().getStartPosition(),
                            MediaPlayer.SEEK_CLOSEST);
                    // In this case, PREPARED notification will be sent when seek is done.
                } else {
                    // The content is not seekable. e.g. live contents.
                    preparedListener.onPrepared(mp);
                }
            }
        });
        p.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
            @Override
            public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        cb.onVideoSizeChanged(MediaPlayer2Impl.this, src.getDSD(), width, height);
                    }
                });
            }
        });
        p.setOnInfoListener(new MediaPlayer.OnInfoListener() {
            @Override
            public boolean onInfo(final MediaPlayer mp, final int what, final int extra) {
                switch (what) {
                    case MediaPlayer.MEDIA_INFO_BUFFERING_START:
                        mPlayer.setBufferingState(
                                mp, SessionPlayer.BUFFERING_STATE_BUFFERING_AND_STARVED);
                        break;
                    case MediaPlayer.MEDIA_INFO_BUFFERING_END:
                        mPlayer.setBufferingState(
                                mp, SessionPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
                        break;
                }
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        if (what == MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT) {
                            mPlayer.onStartedAsNext(mp);
                            return;
                        }
                        int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
                        cb.onInfo(MediaPlayer2Impl.this, src.getDSD(), w, extra);
                    }
                });
                return true;
            }
        });
        final MediaPlayer.OnCompletionListener completionListener =
                new MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(MediaPlayer mp) {
                        handleDataSourceError(mPlayer.onCompletion(mp));
                    }
                };
        p.setOnCompletionListener(completionListener);
        p.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, final int what, final int extra) {
                mPlayer.onError(mp);
                synchronized (mTaskLock) {
                    if (mCurrentTask != null
                            && mCurrentTask.mNeedToWaitForEventToComplete) {
                        mCurrentTask.sendCompleteNotification(CALL_STATUS_ERROR_UNKNOWN);
                        mCurrentTask = null;
                        processPendingTask_l();
                    }
                }
                final int w  = (what == MediaPlayer.MEDIA_ERROR_UNKNOWN)
                        ? sErrorEventExtraMap.getOrDefault(extra, MEDIA_ERROR_UNKNOWN)
                        : MEDIA_ERROR_UNKNOWN;
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        cb.onError(MediaPlayer2Impl.this, src.getDSD(), w, 0);
                    }
                });
                return true;
            }
        });
        p.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
            @Override
            public void onSeekComplete(MediaPlayer mp) {
                if (src.mMp2State == PLAYER_STATE_IDLE) {
                    // This seek request was for handling start position. Notify client that it's
                    // ready to start playback.
                    preparedListener.onPrepared(mp);
                    return;
                }
                synchronized (mTaskLock) {
                    if (mCurrentTask != null
                            && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
                            && mCurrentTask.mNeedToWaitForEventToComplete) {
                        mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
                        mCurrentTask = null;
                        processPendingTask_l();
                    }
                }
            }
        });
        p.setOnTimedMetaDataAvailableListener(
                new MediaPlayer.OnTimedMetaDataAvailableListener() {
                    @Override
                    public void onTimedMetaDataAvailable(MediaPlayer mp, final android.media
                            .TimedMetaData data) {
                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
                            @Override
                            public void notify(EventCallback cb) {
                                cb.onTimedMetaDataAvailable(MediaPlayer2Impl.this, src.getDSD(),
                                        new TimedMetaData(data));
                            }
                        });
                    }
                });
        p.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
            @Override
            public void onBufferingUpdate(MediaPlayer mp, final int percent) {
                if (percent >= 100) {
                    mPlayer.setBufferingState(
                            mp, SessionPlayer.BUFFERING_STATE_COMPLETE);
                }
                src.mBufferedPercentage.set(percent);
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        cb.onInfo(MediaPlayer2Impl.this, src.getDSD(),
                                MEDIA_INFO_BUFFERING_UPDATE, percent);
                    }
                });
            }
        });
        p.setOnMediaTimeDiscontinuityListener(
                new MediaPlayer.OnMediaTimeDiscontinuityListener() {
                    @Override
                    public void onMediaTimeDiscontinuity(
                            MediaPlayer mp, final android.media.MediaTimestamp timestamp) {
                        notifyMediaPlayer2Event(new Mp2EventNotifier() {
                            @Override
                            public void notify(EventCallback cb) {
                                cb.onMediaTimeDiscontinuity(
                                        MediaPlayer2Impl.this, src.getDSD(),
                                        new MediaTimestamp(timestamp));
                            }
                        });
                        setEndPositionTimerIfNeeded(completionListener, src, timestamp);
                    }
                });
        p.setOnSubtitleDataListener(new MediaPlayer.OnSubtitleDataListener() {
            @Override
            public  void onSubtitleData(MediaPlayer mp, final android.media.SubtitleData data) {
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        cb.onSubtitleData(
                                MediaPlayer2Impl.this, src.getDSD(), new SubtitleData(data));
                    }
                });
            }
        });
        p.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
            @Override
            public void onDrmInfo(MediaPlayer mp, final MediaPlayer.DrmInfo drmInfo) {
                notifyDrmEvent(new DrmEventNotifier() {
                    @Override
                    public void notify(DrmEventCallback cb) {
                        cb.onDrmInfo(MediaPlayer2Impl.this, src.getDSD(),
                                new DrmInfoImpl(drmInfo.getPssh(), drmInfo.getSupportedSchemes()));
                    }
                });
            }
        });
    }

    /**
     * Encapsulates the DRM properties of the source.
     */
    public static final class DrmInfoImpl extends DrmInfo {
        private Map<UUID, byte[]> mMapPssh;
        private UUID[] mSupportedSchemes;

        /**
         * Returns the PSSH info of the media item for each supported DRM scheme.
         */
        @Override
        public Map<UUID, byte[]> getPssh() {
            return mMapPssh;
        }

        /**
         * 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.
         */
        @Override
        public List<UUID> getSupportedSchemes() {
            return Arrays.asList(mSupportedSchemes);
        }

        DrmInfoImpl(Map<UUID, byte[]> pssh, UUID[] supportedSchemes) {
            mMapPssh = pssh;
            mSupportedSchemes = supportedSchemes;
        }

        private DrmInfoImpl(Parcel parcel) {
            Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize());

            int psshsize = parcel.readInt();
            byte[] pssh = new byte[psshsize];
            parcel.readByteArray(pssh);

            Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh));
            mMapPssh = parsePSSH(pssh, psshsize);
            Log.v(TAG, "DrmInfoImpl() PSSH: " + mMapPssh);

            int supportedDRMsCount = parcel.readInt();
            mSupportedSchemes = new UUID[supportedDRMsCount];
            for (int i = 0; i < supportedDRMsCount; i++) {
                byte[] uuid = new byte[16];
                parcel.readByteArray(uuid);

                mSupportedSchemes[i] = bytesToUUID(uuid);

                Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: "
                        + mSupportedSchemes[i]);
            }

            Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize
                    + " supportedDRMsCount: " + supportedDRMsCount);
        }

        private DrmInfoImpl makeCopy() {
            return new DrmInfoImpl(this.mMapPssh, this.mSupportedSchemes);
        }

        private String arrToHex(byte[] bytes) {
            String out = "0x";
            for (int i = 0; i < bytes.length; i++) {
                out += String.format("%02x", bytes[i]);
            }

            return out;
        }

        private UUID bytesToUUID(byte[] uuid) {
            long msb = 0, lsb = 0;
            for (int i = 0; i < 8; i++) {
                msb |= (((long) uuid[i] & 0xff) << (8 * (7 - i)));
                lsb |= (((long) uuid[i + 8] & 0xff) << (8 * (7 - i)));
            }

            return new UUID(msb, lsb);
        }

        private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) {
            Map<UUID, byte[]> result = new HashMap<UUID, byte[]>();

            final int uuidSize = 16;
            final int dataLenSize = 4;

            int len = psshsize;
            int numentries = 0;
            int i = 0;

            while (len > 0) {
                if (len < uuidSize) {
                    Log.w(TAG, String.format("parsePSSH: len is too short to parse "
                            + "UUID: (%d < 16) pssh: %d", len, psshsize));
                    return null;
                }

                byte[] subset = Arrays.copyOfRange(pssh, i, i + uuidSize);
                UUID uuid = bytesToUUID(subset);
                i += uuidSize;
                len -= uuidSize;

                // get data length
                if (len < 4) {
                    Log.w(TAG, String.format("parsePSSH: len is too short to parse "
                            + "datalen: (%d < 4) pssh: %d", len, psshsize));
                    return null;
                }

                subset = Arrays.copyOfRange(pssh, i, i + dataLenSize);
                int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN)
                        ? ((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16)
                        | ((subset[1] & 0xff) << 8) | (subset[0] & 0xff)
                        : ((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16)
                                | ((subset[2] & 0xff) << 8) | (subset[3] & 0xff);
                i += dataLenSize;
                len -= dataLenSize;

                if (len < datalen) {
                    Log.w(TAG, String.format("parsePSSH: len is too short to parse "
                            + "data: (%d < %d) pssh: %d", len, datalen, psshsize));
                    return null;
                }

                byte[] data = Arrays.copyOfRange(pssh, i, i + datalen);

                // skip the data
                i += datalen;
                len -= datalen;

                Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d",
                        numentries, uuid, arrToHex(data), psshsize));
                numentries++;
                result.put(uuid, data);
            }

            return result;
        }

    };  // DrmInfoImpl

    private abstract class Task implements Runnable {
        final int mMediaCallType;
        final boolean mNeedToWaitForEventToComplete;
        MediaItem mDSD;
        @GuardedBy("this")
        boolean mDone;

        Task(int mediaCallType, boolean needToWaitForEventToComplete) {
            mMediaCallType = mediaCallType;
            mNeedToWaitForEventToComplete = needToWaitForEventToComplete;
        }

        abstract void process() throws IOException, NoDrmSchemeException;

        @Override
        public void run() {
            int status = CALL_STATUS_NO_ERROR;
            boolean skip = false;
            if (mMediaCallType == CALL_COMPLETED_SEEK_TO) {
                synchronized (mTaskLock) {
                    Task next = mPendingTasks.peekFirst();
                    if (next != null && next.mMediaCallType == CALL_COMPLETED_SEEK_TO) {
                        skip = true;
                    }
                }
            }
            if (!skip) {
                try {
                    if (mMediaCallType != CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED
                            && getState() == PLAYER_STATE_ERROR) {
                        status = CALL_STATUS_INVALID_OPERATION;
                    } else {
                        process();
                    }
                } catch (IllegalStateException e) {
                    status = CALL_STATUS_INVALID_OPERATION;
                } catch (IllegalArgumentException e) {
                    status = CALL_STATUS_BAD_VALUE;
                } catch (SecurityException e) {
                    status = CALL_STATUS_PERMISSION_DENIED;
                } catch (IOException e) {
                    status = CALL_STATUS_ERROR_IO;
                } catch (Exception e) {
                    status = CALL_STATUS_ERROR_UNKNOWN;
                }
            } else {
                status = CALL_STATUS_SKIPPED;
            }

            mDSD = getCurrentMediaItem();

            if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR || skip) {

                sendCompleteNotification(status);

                synchronized (mTaskLock) {
                    mCurrentTask = null;
                    processPendingTask_l();
                }
            }
            // reset() might be waiting for this task. Notify that the task is done.
            synchronized (this) {
                mDone = true;
                this.notifyAll();
            }
        }

        void sendCompleteNotification(final int status) {
            if (mMediaCallType >= SEPARATE_CALL_COMPLETE_CALLBACK_START) {
                // These methods have a separate call complete callback and it should be already
                // called within {@link #processs()}.
                return;
            }
            notifyMediaPlayer2Event(new Mp2EventNotifier() {
                @Override
                public void notify(EventCallback cb) {
                    cb.onCallCompleted(
                            MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
                }
            });
        }
    };

    private static class DataSourceError {
        final MediaItem mDSD;
        final int mWhat;

        final int mExtra;
        DataSourceError(MediaItem item, int what, int extra) {
            mDSD = item;
            mWhat = what;
            mExtra = extra;
        }

    }

    private class MediaPlayerSource {

        volatile MediaItem mDSD;
        MediaPlayer mPlayer;
        final AtomicInteger mBufferedPercentage = new AtomicInteger(0);
        int mSourceState = SOURCE_STATE_INIT;
        @MediaPlayer2State int mMp2State = PLAYER_STATE_IDLE;
        @BuffState int mBufferingState = SessionPlayer.BUFFERING_STATE_UNKNOWN;
        @PlayerState int mPlayerState = SessionPlayer.PLAYER_STATE_IDLE;
        boolean mPlayPending;
        boolean mSetAsNextPlayer;
        final Map<Integer, Integer> mSelectedTracks = new ArrayMap<>();

        MediaPlayerSource(final MediaItem item) {
            mDSD = item;
            setUpListeners(this);
        }

        MediaItem getDSD() {
            return mDSD;
        }

        synchronized MediaPlayer getPlayer() {
            if (mPlayer == null) {
                mPlayer = new MediaPlayer();
            }
            return mPlayer;
        }

        void release() {
            clearListeners(this);
            mPlayer.release();
        }

        void selectTrack(int index) {
            final MediaPlayer mp = getPlayer();
            mp.selectTrack(index);
            MediaPlayer.TrackInfo[] trackInfos = mp.getTrackInfo();
            if (index >= 0 && index < trackInfos.length) {
                mSelectedTracks.put(trackInfos[index].getTrackType(), index);
            }
        }

        void deselectTrack(int index) {
            final MediaPlayer mp = getPlayer();
            mp.deselectTrack(index);
            MediaPlayer.TrackInfo[] trackInfos = mp.getTrackInfo();
            if (index >= 0 && index < trackInfos.length) {
                int trackType = trackInfos[index].getTrackType();
                if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
                        || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
                    mSelectedTracks.remove(trackType);
                }
            }
        }

        int getSelectedTrack(int trackType) {
            return mSelectedTracks.getOrDefault(trackType, -1);
        }
    }

    private class MediaPlayerSourceQueue {
        List<MediaPlayerSource> mQueue = new ArrayList<>();
        Float mVolume = 1.0f;
        Surface mSurface;
        Integer mAuxEffect;
        Float mAuxEffectSendLevel;
        AudioAttributesCompat mAudioAttributes;
        Integer mAudioSessionId;
        android.media.PlaybackParams mPlaybackParams = DEFAULT_PLAYBACK_PARAMS;
        android.media.PlaybackParams mPlaybackParamsToSetWhenStarting;
        boolean mLooping;

        MediaPlayerSourceQueue() {
            mQueue.add(new MediaPlayerSource(null));
        }

        synchronized MediaPlayer getCurrentPlayer() {
            return mQueue.get(0).getPlayer();
        }

        synchronized MediaPlayerSource getFirst() {
            return mQueue.get(0);
        }

        synchronized void setFirst(MediaItem item) throws IOException {
            if (mQueue.isEmpty()) {
                mQueue.add(0, new MediaPlayerSource(item));
            } else {
                mQueue.get(0).mDSD = item;
                setUpListeners(mQueue.get(0));
            }
            handleDataSource(mQueue.get(0));
        }

        synchronized DataSourceError setNext(MediaItem item) {
            if (mQueue.isEmpty() || getFirst().getDSD() == null) {
                throw new IllegalStateException();
            }
            // Clear next media items if any.
            while (mQueue.size() >= 2) {
                MediaPlayerSource src = mQueue.remove(1);
                src.release();
            }
            MediaPlayerSource src = new MediaPlayerSource(item);
            mQueue.add(1, src);
            return prepareAt(1);
        }

        synchronized DataSourceError setNextMultiple(List<MediaItem> descs) {
            if (mQueue.isEmpty() || getFirst().getDSD() == null) {
                throw new IllegalStateException();
            }
            // Clear next media items if any.
            while (mQueue.size() >= 2) {
                MediaPlayerSource src = mQueue.remove(1);
                src.release();
            }
            List<MediaPlayerSource> sources = new ArrayList<>();
            for (MediaItem item: descs) {
                sources.add(new MediaPlayerSource(item));
            }
            mQueue.addAll(1, sources);
            return prepareAt(1);
        }

        synchronized void play() {
            final MediaPlayerSource src = mQueue.get(0);
            if (src.mSourceState == SOURCE_STATE_PREPARED) {
                if (mPlaybackParamsToSetWhenStarting != null) {
                    src.getPlayer().setPlaybackParams(mPlaybackParamsToSetWhenStarting);
                    mPlaybackParamsToSetWhenStarting = null;
                }
                src.getPlayer().start();
                setMp2State(src.getPlayer(), PLAYER_STATE_PLAYING);
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback callback) {
                        callback.onInfo(MediaPlayer2Impl.this, src.getDSD(),
                                MEDIA_INFO_DATA_SOURCE_START, 0);
                    }
                });
            } else {
                throw new IllegalStateException();
            }
        }

        synchronized void release() {
            getCurrentPlayer().release();
        }

        synchronized void prepareAsync() {
            MediaPlayer mp = getCurrentPlayer();
            mp.prepareAsync();
            setBufferingState(mp, SessionPlayer.BUFFERING_STATE_BUFFERING_AND_STARVED);
        }

        synchronized void pause() {
            MediaPlayerSource current = getFirst();
            if (current.mMp2State == PLAYER_STATE_PREPARED) {
                // MediaPlayer1 does not allow pause() in the prepared state. To workaround, call
                // start() here right before calling pause().
                current.mPlayer.start();
            }
            current.mPlayer.pause();
            setMp2State(current.mPlayer, PLAYER_STATE_PAUSED);
        }

        synchronized long getCurrentPosition() {
            // Throws an ISE here rather than relying on MediaPlayer1 implementation which returns
            // a garbage value in the IDLE state.
            if (getFirst().mMp2State == PLAYER_STATE_IDLE) {
                throw new IllegalStateException();
            }
            return getCurrentPlayer().getCurrentPosition();
        }

        synchronized long getDuration() {
            // Throws an ISE here rather than relying on MediaPlayer1 implementation which returns
            // a garbage value in the IDLE state.
            if (getFirst().mMp2State == PLAYER_STATE_IDLE) {
                throw new IllegalStateException();
            }
            return getCurrentPlayer().getDuration();
        }

        synchronized long getBufferedPosition() {
            // Throws an ISE here rather than relying on MediaPlayer1 implementation which returns
            // a garbage value in the IDLE state.
            if (getFirst().mMp2State == PLAYER_STATE_IDLE) {
                throw new IllegalStateException();
            }
            MediaPlayerSource src = mQueue.get(0);
            return (long) src.getPlayer().getDuration() * src.mBufferedPercentage.get() / 100;
        }

        synchronized void setAudioAttributes(AudioAttributesCompat attributes) {
            mAudioAttributes = attributes;
            AudioAttributes attr = mAudioAttributes == null
                    ? null : (AudioAttributes) mAudioAttributes.unwrap();
            getCurrentPlayer().setAudioAttributes(attr);
        }

        synchronized AudioAttributesCompat getAudioAttributes() {
            return mAudioAttributes;
        }

        synchronized DataSourceError onPrepared(MediaPlayer mp) {
            for (int i = 0; i < mQueue.size(); i++) {
                MediaPlayerSource src = mQueue.get(i);
                if (mp == src.getPlayer()) {
                    if (i == 0) {
                        if (src.mPlayPending) {
                            src.mPlayPending = false;
                            src.getPlayer().start();
                            setMp2State(src.getPlayer(), PLAYER_STATE_PLAYING);
                        } else {
                            setMp2State(src.getPlayer(), PLAYER_STATE_PREPARED);
                        }
                    }
                    src.mSourceState = SOURCE_STATE_PREPARED;
                    setBufferingState(src.getPlayer(),
                            SessionPlayer.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
                    if (i == 1) {
                        boolean hasVideo = false;
                        for (MediaPlayer.TrackInfo info : mp.getTrackInfo()) {
                            if (info.getTrackType()
                                    == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
                                hasVideo = true;
                                break;
                            }
                        }
                        // setNextMediaPlayer() does not pass surface to next player. Use it only
                        // for the audio-only media item.
                        if (!hasVideo) {
                            getCurrentPlayer().setNextMediaPlayer(src.mPlayer);
                            src.mSetAsNextPlayer = true;
                        }
                    }
                    return prepareAt(i + 1);
                }
            }
            return null;
        }

        synchronized DataSourceError onCompletion(MediaPlayer mp) {
            final MediaPlayerSource src = getFirst();
            if (mLooping && mp == src.mPlayer) {
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
                        MediaItem item = src.getDSD();
                        cb.onInfo(mp2, item, MEDIA_INFO_DATA_SOURCE_REPEAT, 0);
                    }
                });
                src.mPlayer.seekTo((int) src.getDSD().getStartPosition());
                src.mPlayer.start();
                setMp2State(mp, PLAYER_STATE_PLAYING);
                return null;
            }
            if (mp == src.mPlayer) {
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback cb) {
                        MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
                        MediaItem item = src.getDSD();
                        cb.onInfo(mp2, item, MEDIA_INFO_DATA_SOURCE_END, 0);
                    }
                });
            } else {
                Log.w(TAG, "Playback complete event from next player. Ignoring.");
            }
            if (!mQueue.isEmpty() && mp == src.mPlayer) {
                if (mQueue.size() == 1) {
                    setMp2State(mp, PLAYER_STATE_PAUSED);

                    final MediaItem item = mQueue.get(0).getDSD();
                    notifyMediaPlayer2Event(new Mp2EventNotifier() {
                        @Override
                        public void notify(EventCallback callback) {
                            callback.onInfo(MediaPlayer2Impl.this, item,
                                    MEDIA_INFO_DATA_SOURCE_LIST_END, 0);
                        }
                    });
                    return null;
                } else {
                    if (mQueue.get(1).mSetAsNextPlayer) {
                        // Transition to next player will be handled by MEDIA_INFO_STARTED_AS_NEXT
                        // event later.
                        return null;
                    }
                    moveToNext();
                    return playCurrent();
                }
            } else {
                Log.w(TAG, "Invalid playback complete callback from " + mp.toString());
                return null;
            }
        }

        synchronized void onStartedAsNext(MediaPlayer mp) {
            if (mQueue.size() >= 2 && mQueue.get(1).mPlayer == mp) {
                moveToNext();
                final MediaPlayerSource src = getFirst();
                setMp2State(src.getPlayer(), PLAYER_STATE_PLAYING);
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback callback) {
                        callback.onInfo(MediaPlayer2Impl.this, src.getDSD(),
                                MEDIA_INFO_DATA_SOURCE_START, 0);
                    }
                });
                prepareAt(1);
                applyProperties();
            }
        }

        synchronized void moveToNext() {
            final MediaPlayerSource src1 = mQueue.remove(0);
            src1.release();
            if (mQueue.isEmpty()) {
                throw new IllegalStateException("player/source queue emptied");
            }
        }

        synchronized DataSourceError playCurrent() {
            DataSourceError err = null;
            applyProperties();

            final MediaPlayerSource src = mQueue.get(0);
            if (src.mSourceState == SOURCE_STATE_PREPARED) {
                // start next source only when it's in prepared state.
                src.getPlayer().start();
                setMp2State(src.getPlayer(), PLAYER_STATE_PLAYING);
                notifyMediaPlayer2Event(new Mp2EventNotifier() {
                    @Override
                    public void notify(EventCallback callback) {
                        callback.onInfo(MediaPlayer2Impl.this, src.getDSD(),
                                MEDIA_INFO_DATA_SOURCE_START, 0);
                    }
                });
                prepareAt(1);

            } else {
                if (src.mSourceState == SOURCE_STATE_INIT) {
                    err = prepareAt(0);
                }
                src.mPlayPending = true;
            }
            return err;
        }

        synchronized void applyProperties() {
            final MediaPlayerSource src = mQueue.get(0);
            if (mSurface != null) {
                src.getPlayer().setSurface(mSurface);
            }
            if (mVolume != null) {
                src.getPlayer().setVolume(mVolume, mVolume);
            }
            if (mAudioAttributes != null) {
                src.getPlayer().setAudioAttributes((AudioAttributes) mAudioAttributes.unwrap());
            }
            if (mAuxEffect != null) {
                src.getPlayer().attachAuxEffect(mAuxEffect);
            }
            if (mAuxEffectSendLevel != null) {
                src.getPlayer().setAuxEffectSendLevel(mAuxEffectSendLevel);
            }
            if (mPlaybackParams != DEFAULT_PLAYBACK_PARAMS) {
                src.getPlayer().setPlaybackParams(mPlaybackParams);
            }
        }

        synchronized void onError(MediaPlayer mp) {
            setMp2State(mp, PLAYER_STATE_ERROR);
            setBufferingState(mp, SessionPlayer.BUFFERING_STATE_UNKNOWN);
        }

        synchronized DataSourceError prepareAt(int n) {
            if (n >= Math.min(2, mQueue.size())
                    || mQueue.get(n).mSourceState != SOURCE_STATE_INIT
                    || (n != 0 && getPlayerState() == SessionPlayer.PLAYER_STATE_IDLE)) {
                // There is no next source or it's in preparing or prepared state.
                return null;
            }

            MediaPlayerSource src = mQueue.get(n);
            try {
                // Apply audio session ID before calling setDataSource().
                if (mAudioSessionId != null) {
                    src.getPlayer().setAudioSessionId(mAudioSessionId);
                }
                src.mSourceState = SOURCE_STATE_PREPARING;
                handleDataSource(src);
                src.getPlayer().prepareAsync();
                return null;
            } catch (Exception e) {
                MediaItem item = src.getDSD();
                setMp2State(src.getPlayer(), PLAYER_STATE_ERROR);
                return new DataSourceError(item, MEDIA_ERROR_UNKNOWN, 0);
            }

        }

        synchronized void skipToNext() {
            if (mQueue.size() <= 1) {
                throw new IllegalStateException("No next source available");
            }
            final MediaPlayerSource src = mQueue.get(0);
            moveToNext();
            if (src.mPlayerState == SessionPlayer.PLAYER_STATE_PLAYING || src.mPlayPending) {
                playCurrent();
            }
        }

        synchronized void setLooping(boolean loop) {
            mLooping = loop;
        }

        synchronized void setPlaybackParams(final android.media.PlaybackParams params) {
            if (params == null || params.getSpeed() == 0f) {
                throw new IllegalArgumentException();
            }
            android.media.PlaybackParams current = getPlaybackParams();
            MediaPlayerSource firstPlayer = mPlayer.getFirst();
            if (firstPlayer.mMp2State != PLAYER_STATE_PLAYING) {
                // MediaPlayer1 may start the playback on setPlaybackParams. Store the value here
                // so that it can be applied later when starting the playback.
                mPlaybackParamsToSetWhenStarting = params;
            } else {
                firstPlayer.mPlayer.setPlaybackParams(params);
                mPlaybackParamsToSetWhenStarting = null;
            }

            mPlaybackParams = params;
        }

        synchronized float getVolume() {
            return mVolume;
        }

        synchronized void setVolume(float volume) {
            mVolume = volume;
            getCurrentPlayer().setVolume(volume, volume);
        }

        synchronized void setSurface(Surface surface) {
            mSurface = surface;
            getCurrentPlayer().setSurface(surface);
        }

        synchronized int getVideoWidth() {
            try {
                return getCurrentPlayer().getVideoWidth();
            } catch (IllegalStateException e) {
                return 0;
            }
        }

        synchronized int getVideoHeight() {
            try {
                return getCurrentPlayer().getVideoHeight();
            } catch (IllegalStateException e) {
                return 0;
            }
        }

        synchronized PersistableBundle getMetrics() {
            return getCurrentPlayer().getMetrics();
        }

        synchronized android.media.PlaybackParams getPlaybackParams() {
            // PlaybackParams is mutable. Make a copy of mPlaybackParams and return.
            Parcel parcel = Parcel.obtain();
            mPlaybackParams.writeToParcel(parcel, 0);
            parcel.setDataPosition(0);
            android.media.PlaybackParams ret =
                    android.media.PlaybackParams.CREATOR.createFromParcel(parcel);
            parcel.recycle();
            return ret;
        }

        synchronized void seekTo(long msec, int mode) {
            MediaItem current = getFirst().getDSD();
            Preconditions.checkArgument(
                    current.getStartPosition() <= msec && current.getEndPosition() >= msec,
                    "Requested seek position is out of range : " + msec);
            getCurrentPlayer().seekTo(msec, mode);
        }

        synchronized void reset() {
            mVolume = 1.0f;
            mSurface = null;
            mAuxEffect = null;
            mAuxEffectSendLevel = null;
            mAudioAttributes = null;
            mAudioSessionId = null;
            mPlaybackParams = DEFAULT_PLAYBACK_PARAMS;
            mPlaybackParamsToSetWhenStarting = null;
            mLooping = false;

            MediaPlayerSource first = mQueue.get(0);
            setMp2State(first.getPlayer(), PLAYER_STATE_IDLE);
            setBufferingState(first.getPlayer(), SessionPlayer.BUFFERING_STATE_UNKNOWN);

            for (MediaPlayerSource src : mQueue) {
                src.release();
            }
            mQueue.clear();
            mQueue.add(new MediaPlayerSource(null));
        }

        synchronized MediaTimestamp getTimestamp() {
            android.media.MediaTimestamp t = getCurrentPlayer().getTimestamp();
            return (t == null) ? null : new MediaTimestamp(t);
        }

        synchronized void setAudioSessionId(int sessionId) {
            getCurrentPlayer().setAudioSessionId(sessionId);
            mAudioSessionId = Integer.valueOf(sessionId);
        }

        synchronized int getAudioSessionId() {
            return getCurrentPlayer().getAudioSessionId();
        }

        synchronized void attachAuxEffect(int effectId) {
            getCurrentPlayer().attachAuxEffect(effectId);
            mAuxEffect = Integer.valueOf(effectId);
        }

        synchronized void setAuxEffectSendLevel(float level) {
            getCurrentPlayer().setAuxEffectSendLevel(level);
            mAuxEffectSendLevel = Float.valueOf(level);
        }

        synchronized MediaPlayer.TrackInfo[] getTrackInfo() {
            return getCurrentPlayer().getTrackInfo();
        }

        synchronized int getSelectedTrack(int trackType) {
            final MediaPlayerSource currentPlayerSource = mQueue.get(0);
            return currentPlayerSource.getSelectedTrack(trackType);
        }

        synchronized void selectTrack(int index) {
            final MediaPlayerSource currentPlayerSource = mQueue.get(0);
            currentPlayerSource.selectTrack(index);
        }

        synchronized void deselectTrack(int index) {
            final MediaPlayerSource currentPlayerSource = mQueue.get(0);
            currentPlayerSource.deselectTrack(index);
        }

        synchronized MediaPlayer.DrmInfo getDrmInfo() {
            return getCurrentPlayer().getDrmInfo();
        }

        synchronized void prepareDrm(UUID uuid)
                throws ResourceBusyException, MediaPlayer.ProvisioningServerErrorException,
                MediaPlayer.ProvisioningNetworkErrorException, UnsupportedSchemeException {
            getCurrentPlayer().prepareDrm(uuid);
        }

        synchronized void releaseDrm() throws MediaPlayer.NoDrmSchemeException {
            getCurrentPlayer().stop();
            getCurrentPlayer().releaseDrm();
        }

        synchronized byte[] provideKeyResponse(byte[] keySetId, byte[] response)
                throws DeniedByServerException, MediaPlayer.NoDrmSchemeException {
            return getCurrentPlayer().provideKeyResponse(keySetId, response);
        }

        synchronized void restoreKeys(byte[] keySetId) throws MediaPlayer.NoDrmSchemeException {
            getCurrentPlayer().restoreKeys(keySetId);
        }

        synchronized String getDrmPropertyString(String propertyName)
                throws MediaPlayer.NoDrmSchemeException {
            return getCurrentPlayer().getDrmPropertyString(propertyName);
        }

        synchronized void setDrmPropertyString(String propertyName, String value)
                throws MediaPlayer.NoDrmSchemeException {
            getCurrentPlayer().setDrmPropertyString(propertyName, value);
        }

        synchronized void setOnDrmConfigHelper(MediaPlayer.OnDrmConfigHelper onDrmConfigHelper) {
            getCurrentPlayer().setOnDrmConfigHelper(onDrmConfigHelper);
        }

        synchronized MediaDrm.KeyRequest getKeyRequest(byte[] keySetId, byte[] initData,
                String mimeType,
                int keyType, Map<String, String> optionalParameters)
                throws MediaPlayer.NoDrmSchemeException {
            return getCurrentPlayer().getKeyRequest(keySetId, initData, mimeType, keyType,
                    optionalParameters);
        }

        synchronized void setMp2State(MediaPlayer mp, @MediaPlayer2State int mp2State) {
            for (final MediaPlayerSource src: mQueue) {
                if (src.getPlayer() != mp) {
                    continue;
                }
                if (src.mMp2State == mp2State) {
                    return;
                }
                src.mMp2State = mp2State;

                final int playerState = sStateMap.get(mp2State);
                if (src.mPlayerState == playerState) {
                    return;
                }
                src.mPlayerState = playerState;
                return;
            }
        }

        synchronized void setBufferingState(MediaPlayer mp, @BuffState final int state) {
            for (final MediaPlayerSource src: mQueue) {
                if (src.getPlayer() != mp) {
                    continue;
                }
                if (src.mBufferingState == state) {
                    return;
                }
                src.mBufferingState = state;
                return;
            }
        }

        synchronized @MediaPlayer2State int getMediaPlayer2State() {
            return mQueue.get(0).mMp2State;
        }

        synchronized @BuffState int getBufferingState() {
            return mQueue.get(0).mBufferingState;
        }

        synchronized @PlayerState int getPlayerState() {
            return mQueue.get(0).mPlayerState;
        }

        synchronized MediaPlayerSource getSourceForPlayer(MediaPlayer mp) {
            for (MediaPlayerSource src: mQueue) {
                if (src.getPlayer() == mp) {
                    return src;
                }
            }
            return null;
        }
    }
}