summaryrefslogtreecommitdiff
path: root/androidx/media/MediaPlayer2Impl.java
diff options
context:
space:
mode:
Diffstat (limited to 'androidx/media/MediaPlayer2Impl.java')
-rw-r--r--androidx/media/MediaPlayer2Impl.java1982
1 files changed, 1982 insertions, 0 deletions
diff --git a/androidx/media/MediaPlayer2Impl.java b/androidx/media/MediaPlayer2Impl.java
new file mode 100644
index 00000000..3b3e119e
--- /dev/null
+++ b/androidx/media/MediaPlayer2Impl.java
@@ -0,0 +1,1982 @@
+/*
+ * 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.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+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.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.ResourceBusyException;
+import android.media.SyncParams;
+import android.media.TimedMetaData;
+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.Parcelable;
+import android.os.PersistableBundle;
+import android.util.ArrayMap;
+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.core.util.Preconditions;
+
+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.atomic.AtomicInteger;
+
+/**
+ * @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 NEXT_SOURCE_STATE_ERROR = -1;
+ private static final int NEXT_SOURCE_STATE_INIT = 0;
+ private static final int NEXT_SOURCE_STATE_PREPARING = 1;
+ private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+
+ private static ArrayMap<Integer, Integer> sInfoEventMap;
+ private static ArrayMap<Integer, Integer> sErrorEventMap;
+
+ static {
+ sInfoEventMap = new ArrayMap<>();
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
+ sInfoEventMap.put(2 /*MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT*/, MEDIA_INFO_STARTED_AS_NEXT);
+ 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);
+
+ sErrorEventMap = new ArrayMap<>();
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNKNOWN);
+ sErrorEventMap.put(
+ MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+ MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
+ }
+
+ private MediaPlayer mPlayer; // MediaPlayer is thread-safe.
+
+ private final Object mSrcLock = new Object();
+ //--- guarded by |mSrcLock| start
+ private long mSrcIdGenerator = 0;
+ private DataSourceDesc mCurrentDSD;
+ private long mCurrentSrcId = mSrcIdGenerator++;
+ private List<DataSourceDesc> mNextDSDs;
+ private long mNextSrcId = mSrcIdGenerator++;
+ private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ private boolean mNextSourcePlayPending = false;
+ //--- guarded by |mSrcLock| end
+
+ private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
+ private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
+ private volatile float mVolume = 1.0f;
+
+ private HandlerThread mHandlerThread;
+ private final Handler mTaskHandler;
+ private final Object mTaskLock = new Object();
+ @GuardedBy("mTaskLock")
+ private final ArrayDeque<Task> mPendingTasks = new ArrayDeque<>();
+ @GuardedBy("mTaskLock")
+ private Task mCurrentTask;
+
+ private final Object mLock = new Object();
+ //--- guarded by |mLock| start
+ @PlayerState private int mPlayerState;
+ @BuffState private int mBufferingState;
+ private AudioAttributesCompat mAudioAttributes;
+ private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
+ new ArrayList<>();
+ private ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
+ new ArrayMap<>();
+ private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
+ new ArrayList<>();
+ //--- guarded by |mLock| end
+
+ /**
+ * 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();
+ mTaskHandler = new Handler(looper);
+ mPlayer = new MediaPlayer();
+ mPlayerState = PLAYER_STATE_IDLE;
+ mBufferingState = BUFFERING_STATE_UNKNOWN;
+ setUpListeners();
+ }
+
+ /**
+ * 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() {
+ mPlayer.release();
+ }
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * been stopped, or never started before, playback will start at the
+ * beginning.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void play() {
+ addTask(new Task(CALL_COMPLETED_PLAY, false) {
+ @Override
+ void process() {
+ mPlayer.start();
+ setPlayerState(PLAYER_STATE_PLAYING);
+ }
+ });
+ }
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare(). For streams, you should call prepare(),
+ * which returns immediately, rather than blocking until enough data has been
+ * buffered.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void prepare() {
+ addTask(new Task(CALL_COMPLETED_PREPARE, true) {
+ @Override
+ void process() throws IOException {
+ mPlayer.prepareAsync();
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+ }
+ });
+ }
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been initialized.
+ */
+ @Override
+ public void pause() {
+ addTask(new Task(CALL_COMPLETED_PAUSE, false) {
+ @Override
+ void process() {
+ mPlayer.pause();
+ setPlayerState(PLAYER_STATE_PAUSED);
+ }
+ });
+ }
+
+ /**
+ * Tries to play next data source if applicable.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void skipToNext() {
+ addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
+ @Override
+ void process() {
+ // TODO: switch to next data source and play
+ }
+ });
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ @Override
+ public long getCurrentPosition() {
+ return mPlayer.getCurrentPosition();
+ }
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ @Override
+ public long getDuration() {
+ return mPlayer.getDuration();
+ }
+
+ /**
+ * 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.
+ *
+ * @return the current buffered media source position in milliseconds
+ */
+ @Override
+ public long getBufferedPosition() {
+ // Use cached buffered percent for now.
+ return getDuration() * mBufferedPercentageCurrent.get() / 100;
+ }
+
+ @Override
+ public @PlayerState int getPlayerState() {
+ synchronized (mLock) {
+ return mPlayerState;
+ }
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ */
+ @Override
+ public @BuffState int getBufferingState() {
+ synchronized (mLock) {
+ return mBufferingState;
+ }
+ }
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepare()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ * @throws IllegalArgumentException if the attributes are null or invalid.
+ */
+ @Override
+ public void setAudioAttributes(@NonNull final AudioAttributesCompat attributes) {
+ addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
+ @Override
+ void process() {
+ AudioAttributes attr;
+ synchronized (mLock) {
+ mAudioAttributes = attributes;
+ attr = (AudioAttributes) mAudioAttributes.unwrap();
+ }
+ mPlayer.setAudioAttributes(attr);
+ }
+ });
+ }
+
+ @Override
+ public @NonNull AudioAttributesCompat getAudioAttributes() {
+ synchronized (mLock) {
+ return mAudioAttributes;
+ }
+ }
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setDataSource(@NonNull final DataSourceDesc dsd) {
+ addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ // TODO: setDataSource could update exist data source
+ synchronized (mSrcLock) {
+ mCurrentDSD = dsd;
+ mCurrentSrcId = mSrcIdGenerator++;
+ try {
+ handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
+ } catch (IOException e) {
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets a single data source as described by a DataSourceDesc which will be played
+ * after current data source is finished.
+ *
+ * @param dsd the descriptor of data source you want to play after current one
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setNextDataSource(@NonNull final DataSourceDesc dsd) {
+ addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ synchronized (mSrcLock) {
+ mNextDSDs = new ArrayList<DataSourceDesc>(1);
+ mNextDSDs.add(dsd);
+ mNextSrcId = mSrcIdGenerator++;
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
+ }
+ /* FIXME : define and handle state.
+ int state = getMediaPlayer2State();
+ if (state != MEDIAPLAYER2_STATE_IDLE) {
+ synchronized (mSrcLock) {
+ prepareNextDataSource_l();
+ }
+ }
+ */
+ }
+ });
+ }
+
+ /**
+ * Sets a list of data sources to be played sequentially after current data source is done.
+ *
+ * @param dsds the list of data sources you want to play after current one
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if dsds is null or empty, or contains null DataSourceDesc
+ */
+ @Override
+ public void setNextDataSources(@NonNull final List<DataSourceDesc> dsds) {
+ addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
+ @Override
+ void process() {
+ if (dsds == null || dsds.size() == 0) {
+ throw new IllegalArgumentException("data source list cannot be null or empty.");
+ }
+ for (DataSourceDesc dsd : dsds) {
+ if (dsd == null) {
+ throw new IllegalArgumentException(
+ "DataSourceDesc in the source list cannot be null.");
+ }
+ }
+
+ synchronized (mSrcLock) {
+ mNextDSDs = new ArrayList(dsds);
+ mNextSrcId = mSrcIdGenerator++;
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
+ }
+ /* FIXME : define and handle state.
+ int state = getMediaPlayer2State();
+ if (state != MEDIAPLAYER2_STATE_IDLE) {
+ synchronized (mSrcLock) {
+ prepareNextDataSource_l();
+ }
+ }
+ */
+ }
+ });
+ }
+
+ @Override
+ public @NonNull DataSourceDesc getCurrentDataSource() {
+ synchronized (mSrcLock) {
+ return mCurrentDSD;
+ }
+ }
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ @Override
+ public void loopCurrent(final boolean loop) {
+ addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
+ @Override
+ void process() {
+ mPlayer.setLooping(loop);
+ }
+ });
+ }
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed the desired playback speed
+ */
+ @Override
+ public void setPlaybackSpeed(final float speed) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
+ @Override
+ void process() {
+ setPlaybackParamsInternal(getPlaybackParams().setSpeed(speed));
+ }
+ });
+ }
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return getPlaybackParams().getSpeed();
+ }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ @Override
+ public boolean isReversePlaybackSupported() {
+ return false;
+ }
+
+ /**
+ * 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.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ @Override
+ public void setPlayerVolume(final float volume) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
+ @Override
+ void process() {
+ mVolume = volume;
+ mPlayer.setVolume(volume, volume);
+ }
+ });
+ }
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ @Override
+ public float getPlayerVolume() {
+ return mVolume;
+ }
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ @Override
+ public float getMaxPlayerVolume() {
+ return 1.0f;
+ }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ @Override
+ public void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb) {
+ if (cb == null) {
+ throw new IllegalArgumentException("Illegal null PlayerEventCallback");
+ }
+ if (e == null) {
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the PlayerEventCallback");
+ }
+ synchronized (mLock) {
+ mPlayerEventCallbackMap.put(cb, e);
+ }
+ }
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ @Override
+ public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+ if (cb == null) {
+ throw new IllegalArgumentException("Illegal null PlayerEventCallback");
+ }
+ synchronized (mLock) {
+ mPlayerEventCallbackMap.remove(cb);
+ }
+ }
+
+ @Override
+ public void notifyWhenCommandLabelReached(final Object label) {
+ addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
+ @Override
+ void process() {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Sets the {@link Surface} 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 {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ @Override
+ public void setSurface(final Surface surface) {
+ addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
+ @Override
+ void process() {
+ mPlayer.setSurface(surface);
+ }
+ });
+ }
+
+ /**
+ * Discards all pending commands.
+ */
+ @Override
+ public void clearPendingCommands() {
+ // TODO: implement this.
+ }
+
+ private void addTask(Task task) {
+ synchronized (mTaskLock) {
+ mPendingTasks.add(task);
+ processPendingTask_l();
+ }
+ }
+
+ @GuardedBy("mTaskLock")
+ private void processPendingTask_l() {
+ if (mCurrentTask != null) {
+ return;
+ }
+ if (!mPendingTasks.isEmpty()) {
+ Task task = mPendingTasks.removeFirst();
+ mCurrentTask = task;
+ mTaskHandler.post(task);
+ }
+ }
+
+ private void handleDataSource(boolean isCurrent, @NonNull final DataSourceDesc dsd, long srcId)
+ throws IOException {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+ // TODO: handle the case isCurrent is false.
+ switch (dsd.getType()) {
+ case DataSourceDesc.TYPE_CALLBACK:
+ mPlayer.setDataSource(new MediaDataSource() {
+ Media2DataSource mDataSource = dsd.getMedia2DataSource();
+ @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();
+ }
+ });
+ break;
+
+ case DataSourceDesc.TYPE_FD:
+ mPlayer.setDataSource(
+ dsd.getFileDescriptor(),
+ dsd.getFileDescriptorOffset(),
+ dsd.getFileDescriptorLength());
+ break;
+
+ case DataSourceDesc.TYPE_URI:
+ mPlayer.setDataSource(
+ dsd.getUriContext(),
+ dsd.getUri(),
+ dsd.getUriHeaders(),
+ dsd.getUriCookies());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return 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 {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+ * is available.
+ */
+ @Override
+ public int getVideoWidth() {
+ return mPlayer.getVideoWidth();
+ }
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return 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 {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height
+ * is available.
+ */
+ @Override
+ public int getVideoHeight() {
+ return mPlayer.getVideoHeight();
+ }
+
+ @Override
+ public PersistableBundle getMetrics() {
+ return mPlayer.getMetrics();
+ }
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @throws IllegalArgumentException if params is not supported.
+ */
+ @Override
+ public void setPlaybackParams(@NonNull final PlaybackParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
+ @Override
+ void process() {
+ setPlaybackParamsInternal(params);
+ }
+ });
+ }
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public PlaybackParams getPlaybackParams() {
+ return mPlayer.getPlaybackParams();
+ }
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if params are not supported.
+ */
+ @Override
+ public void setSyncParams(@NonNull final SyncParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
+ @Override
+ void process() {
+ mPlayer.setSyncParams(params);
+ }
+ });
+ }
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public SyncParams getSyncParams() {
+ return mPlayer.getSyncParams();
+ }
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * 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.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * 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.
+ * @param mode the mode indicating where exactly to seek to.
+ * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp earlier than or the same as msec. Use
+ * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp later than or the same as msec. Use
+ * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp closest to or the same as msec. Use
+ * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+ * or may not be a sync frame but is closest to or the same as msec.
+ * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+ * to the other options if there is no sync frame located at msec.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ @Override
+ public void seekTo(final long msec, @SeekMode final int mode) {
+ addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
+ @Override
+ void process() {
+ mPlayer.seekTo(msec, mode);
+ }
+ });
+ }
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ * @see MediaTimestamp
+ */
+ @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
+ * data source and calling prepare().
+ */
+ @Override
+ public void reset() {
+ mPlayer.reset();
+ setPlayerState(PLAYER_STATE_IDLE);
+ setBufferingState(BUFFERING_STATE_UNKNOWN);
+ /* FIXME: reset other internal variables. */
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the sessionId is invalid.
+ */
+ @Override
+ public void setAudioSessionId(final int sessionId) {
+ addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
+ @Override
+ void process() {
+ mPlayer.setAudioSessionId(sessionId);
+ }
+ });
+ }
+
+ @Override
+ public int getAudioSessionId() {
+ return mPlayer.getAudioSessionId();
+ }
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ @Override
+ public void attachAuxEffect(final int effectId) {
+ addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
+ @Override
+ void process() {
+ mPlayer.attachAuxEffect(effectId);
+ }
+ });
+ }
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ @Override
+ public void setAuxEffectSendLevel(final float level) {
+ addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
+ @Override
+ void process() {
+ mPlayer.setAuxEffectSendLevel(level);
+ }
+ });
+ }
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see MediaPlayer2#getTrackInfo
+ */
+ public static final class TrackInfoImpl extends TrackInfo {
+ final int mTrackType;
+ final MediaFormat mFormat;
+
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ @Override
+ public int getTrackType() {
+ return mTrackType;
+ }
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ @Override
+ public String getLanguage() {
+ String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
+ return language == null ? "und" : language;
+ }
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ @Override
+ public MediaFormat getFormat() {
+ if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
+ || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ return mFormat;
+ }
+ return null;
+ }
+
+ TrackInfoImpl(Parcel in) {
+ mTrackType = in.readInt();
+ // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
+ // even for audio/video tracks, meaning we only set the mime and language.
+ String mime = in.readString();
+ String language = in.readString();
+ mFormat = MediaFormat.createSubtitleFormat(mime, language);
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
+ }
+ }
+
+ TrackInfoImpl(int type, MediaFormat format) {
+ mTrackType = type;
+ mFormat = format;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ /* package private */ void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTrackType);
+ dest.writeString(getLanguage());
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder(128);
+ out.append(getClass().getName());
+ out.append('{');
+ switch (mTrackType) {
+ case MEDIA_TRACK_TYPE_VIDEO:
+ out.append("VIDEO");
+ break;
+ case MEDIA_TRACK_TYPE_AUDIO:
+ out.append("AUDIO");
+ break;
+ case MEDIA_TRACK_TYPE_TIMEDTEXT:
+ out.append("TIMEDTEXT");
+ break;
+ case MEDIA_TRACK_TYPE_SUBTITLE:
+ out.append("SUBTITLE");
+ break;
+ default:
+ out.append("UNKNOWN");
+ break;
+ }
+ out.append(", " + mFormat.toString());
+ out.append("}");
+ return out.toString();
+ }
+
+ /**
+ * Used to read a TrackInfoImpl from a Parcel.
+ */
+ /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR =
+ new Parcelable.Creator<TrackInfoImpl>() {
+ @Override
+ public TrackInfoImpl createFromParcel(Parcel in) {
+ return new TrackInfoImpl(in);
+ }
+
+ @Override
+ public TrackInfoImpl[] newArray(int size) {
+ return new TrackInfoImpl[size];
+ }
+ };
+
+ };
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return 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.
+ * @throws IllegalStateException if it is called in an invalid state.
+ */
+ @Override
+ public List<TrackInfo> getTrackInfo() {
+ MediaPlayer.TrackInfo[] list = mPlayer.getTrackInfo();
+ List<TrackInfo> trackList = new ArrayList<>();
+ for (MediaPlayer.TrackInfo info : list) {
+ trackList.add(new TrackInfoImpl(info.getTrackType(), info.getFormat()));
+ }
+ return trackList;
+ }
+
+ /**
+ * 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 {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ @Override
+ public int getSelectedTrack(int trackType) {
+ return mPlayer.getSelectedTrack(trackType);
+ }
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ *
+ * @param 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 {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ * @see MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void selectTrack(final int index) {
+ addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
+ @Override
+ void process() {
+ mPlayer.selectTrack(index);
+ }
+ });
+ }
+
+ /**
+ * Deselect a track.
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @param 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 {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ * @see MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void deselectTrack(final int index) {
+ addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
+ @Override
+ void process() {
+ mPlayer.deselectTrack(index);
+ }
+ });
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void setMediaPlayer2EventCallback(@NonNull Executor executor,
+ @NonNull MediaPlayer2EventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the MediaPlayer2EventCallback");
+ }
+ synchronized (mLock) {
+ mMp2EventCallbackRecords.add(new Pair(executor, eventCallback));
+ }
+ }
+
+ /**
+ * Clears the {@link MediaPlayer2EventCallback}.
+ */
+ @Override
+ public void clearMediaPlayer2EventCallback() {
+ synchronized (mLock) {
+ mMp2EventCallbackRecords.clear();
+ }
+ }
+
+ // Modular DRM begin
+
+ /**
+ * 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 {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ @Override
+ public void setOnDrmConfigHelper(final OnDrmConfigHelper listener) {
+ mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
+ @Override
+ public void onDrmConfig(MediaPlayer mp) {
+ /** FIXME: pass the right DSD. */
+ listener.onDrmConfig(MediaPlayer2Impl.this, null);
+ }
+ });
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void setDrmEventCallback(@NonNull Executor executor,
+ @NonNull DrmEventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the MediaPlayer2EventCallback");
+ }
+ synchronized (mLock) {
+ mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
+ }
+ }
+
+ /**
+ * Clears the {@link DrmEventCallback}.
+ */
+ @Override
+ public void clearDrmEventCallback() {
+ synchronized (mLock) {
+ mDrmEventCallbackRecords.clear();
+ }
+ }
+
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before prepare()
+ */
+ @Override
+ public DrmInfo getDrmInfo() {
+ MediaPlayer.DrmInfo info = mPlayer.getDrmInfo();
+ return info == null ? null : new DrmInfoImpl(info.getPssh(), info.getSupportedSchemes());
+ }
+
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@code 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
+ * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ * @throws IllegalStateException if called before prepare(), or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ @Override
+ public void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException {
+ try {
+ mPlayer.prepareDrm(uuid);
+ } catch (MediaPlayer.ProvisioningNetworkErrorException e) {
+ throw new ProvisioningNetworkErrorException(e.getMessage());
+ } catch (MediaPlayer.ProvisioningServerErrorException e) {
+ throw new ProvisioningServerErrorException(e.getMessage());
+ }
+ }
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ @Override
+ public void releaseDrm() throws NoDrmSchemeException {
+ addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
+ @Override
+ void process() throws NoDrmSchemeException {
+ try {
+ mPlayer.releaseDrm();
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+ });
+ }
+
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideDrmKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @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());
+ }
+ }
+
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideDrmKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreDrmKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link #getDrmKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ @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());
+ }
+ }
+
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ @Override
+ public void restoreDrmKeys(@NonNull final byte[] keySetId)
+ throws NoDrmSchemeException {
+ addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
+ @Override
+ void process() throws NoDrmSchemeException {
+ try {
+ mPlayer.restoreKeys(keySetId);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+ });
+ }
+
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ *
+
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ @NonNull
+ public String getDrmPropertyString(@NonNull String propertyName)
+ throws NoDrmSchemeException {
+ try {
+ return mPlayer.getDrmPropertyString(propertyName);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ *
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @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());
+ }
+ }
+
+ private void setPlaybackParamsInternal(final PlaybackParams params) {
+ PlaybackParams current = mPlayer.getPlaybackParams();
+ mPlayer.setPlaybackParams(params);
+ if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+ }
+ });
+ }
+ }
+
+ private void setPlayerState(@PlayerState final int state) {
+ synchronized (mLock) {
+ if (mPlayerState == state) {
+ return;
+ }
+ mPlayerState = state;
+ }
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
+ }
+ });
+ }
+
+ private void setBufferingState(@BuffState final int state) {
+ synchronized (mLock) {
+ if (mBufferingState == state) {
+ return;
+ }
+ mBufferingState = state;
+ }
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
+ }
+ });
+ }
+
+ private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
+ List<Pair<Executor, MediaPlayer2EventCallback>> records;
+ synchronized (mLock) {
+ records = new ArrayList<>(mMp2EventCallbackRecords);
+ }
+ for (final Pair<Executor, MediaPlayer2EventCallback> record : records) {
+ record.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(record.second);
+ }
+ });
+ }
+ }
+
+ private void notifyPlayerEvent(final PlayerEventNotifier notifier) {
+ ArrayMap<PlayerEventCallback, Executor> map;
+ synchronized (mLock) {
+ map = new ArrayMap<>(mPlayerEventCallbackMap);
+ }
+ final int callbackCount = map.size();
+ for (int i = 0; i < callbackCount; i++) {
+ final Executor executor = map.valueAt(i);
+ final PlayerEventCallback cb = map.keyAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(cb);
+ }
+ });
+ }
+ }
+
+ private interface Mp2EventNotifier {
+ void notify(MediaPlayer2EventCallback callback);
+ }
+
+ private interface PlayerEventNotifier {
+ void notify(PlayerEventCallback callback);
+ }
+
+ private void setUpListeners() {
+ mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ setPlayerState(PLAYER_STATE_PAUSED);
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback callback) {
+ callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
+ }
+ });
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
+ }
+ });
+ synchronized (mTaskLock) {
+ if (mCurrentTask != null
+ && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
+ && mCurrentTask.mDSD == mCurrentDSD
+ && mCurrentTask.mNeedToWaitForEventToComplete) {
+ mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ }
+ });
+ mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
+ @Override
+ public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
+ }
+ });
+ }
+ });
+ mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ switch (what) {
+ case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ MEDIA_INFO_VIDEO_RENDERING_START, 0);
+ }
+ });
+ break;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+ break;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ break;
+ }
+ return false;
+ }
+ });
+ mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ setPlayerState(PLAYER_STATE_PAUSED);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
+ 0);
+ }
+ });
+ }
+ });
+ mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, final int what, final int extra) {
+ setPlayerState(PLAYER_STATE_ERROR);
+ setBufferingState(BUFFERING_STATE_UNKNOWN);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
+ cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+ }
+ });
+ return true;
+ }
+ });
+ mPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ synchronized (mTaskLock) {
+ if (mCurrentTask != null
+ && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
+ && mCurrentTask.mNeedToWaitForEventToComplete) {
+ mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ final long seekPos = getCurrentPosition();
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ // TODO: The actual seeked position might be different from the
+ // requested position. Clarify which one is expected here.
+ cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
+ }
+ });
+ }
+ });
+ mPlayer.setOnTimedMetaDataAvailableListener(
+ new MediaPlayer.OnTimedMetaDataAvailableListener() {
+ @Override
+ public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onTimedMetaDataAvailable(
+ MediaPlayer2Impl.this, mCurrentDSD, data);
+ }
+ });
+ }
+ });
+ mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+ }
+ });
+ return true;
+ }
+ });
+ mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, final int percent) {
+ if (percent >= 100) {
+ setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
+ }
+ mBufferedPercentageCurrent.set(percent);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ MEDIA_INFO_BUFFERING_UPDATE, percent);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * 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 data source for each supported DRM scheme.
+ */
+ @Override
+ public Map<UUID, byte[]> getPssh() {
+ return mMapPssh;
+ }
+
+ /**
+ * Returns the intersection of the data source 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);
+ }
+
+ private 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
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException {
+ public NoDrmSchemeExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningNetworkErrorExceptionImpl
+ extends ProvisioningNetworkErrorException {
+ public ProvisioningNetworkErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningServerErrorExceptionImpl
+ extends ProvisioningServerErrorException {
+ public ProvisioningServerErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private abstract class Task implements Runnable {
+ private final int mMediaCallType;
+ private final boolean mNeedToWaitForEventToComplete;
+ private DataSourceDesc mDSD;
+
+ 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;
+ try {
+ 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 (NoDrmSchemeException e) {
+ status = CALL_STATUS_NO_DRM_SCHEME;
+ } catch (Exception e) {
+ status = CALL_STATUS_ERROR_UNKNOWN;
+ }
+ synchronized (mSrcLock) {
+ mDSD = mCurrentDSD;
+ }
+
+ if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
+
+ sendCompleteNotification(status);
+
+ synchronized (mTaskLock) {
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ }
+
+ private void sendCompleteNotification(final int status) {
+ // In {@link #notifyWhenCommandLabelReached} case, a separate callback
+ // {#link #onCommandLabelReached} is already called in {@code process()}.
+ if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
+ return;
+ }
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onCallCompleted(
+ MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
+ }
+ });
+ }
+ };
+}