public final class

ExoPlayerMediaPlayer2Impl

extends MediaPlayer2

implements androidx.media2.player.exoplayer.ExoPlayerWrapper.Listener

 java.lang.Object

↳MediaPlayer2

↳androidx.media2.player.exoplayer.ExoPlayerMediaPlayer2Impl

Overview

An implementation of MediaPlayer2 based on a repackaged version of ExoPlayer.

Summary

Constructors
publicExoPlayerMediaPlayer2Impl(Context context)

Creates a new ExoPlayer wrapper using the specified context.

Methods
public java.lang.ObjectattachAuxEffect(int effectId)

public booleancancel(java.lang.Object token)

public voidclearDrmEventCallback()

public voidclearEventCallback()

public voidclearPendingCommands()

public voidclose()

public java.lang.ObjectdeselectTrack(int index)

public AudioAttributesCompatgetAudioAttributes()

public intgetAudioSessionId()

public longgetBufferedPosition()

public MediaItemgetCurrentMediaItem()

public longgetCurrentPosition()

public DrmInfogetDrmInfo()

public MediaDrm.KeyRequestgetDrmKeyRequest(byte[] keySetId[], byte[] initData[], java.lang.String mimeType, int keyType, java.util.Map<java.lang.String, java.lang.String> optionalParameters)

public java.lang.StringgetDrmPropertyString(java.lang.String propertyName)

public longgetDuration()

public PersistableBundlegetMetrics()

public PlaybackParamsgetPlaybackParams()

public floatgetPlayerVolume()

public intgetSelectedTrack(int trackType)

public intgetState()

public MediaTimestampgetTimestamp()

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

public intgetVideoHeight()

public intgetVideoWidth()

public java.lang.ObjectloopCurrent(boolean loop)

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

public voidonBandwidthSample(MediaItem mediaItem, int bitrateKbps)

public voidonBufferingEnded(MediaItem mediaItem)

public voidonBufferingStarted(MediaItem mediaItem)

public voidonBufferingUpdate(MediaItem mediaItem, int bufferingPercentage)

public voidonError(MediaItem mediaItem, int what)

public voidonLoop(MediaItem mediaItem)

public voidonMediaItemEnded(MediaItem mediaItem)

public voidonMediaItemStartedAsNext(MediaItem mediaItem)

public voidonMediaTimeDiscontinuity(MediaItem mediaItem, MediaTimestamp mediaTimestamp)

public voidonMetadataChanged(MediaItem mediaItem)

public voidonPlaybackEnded(MediaItem mediaItem)

public voidonPrepared(MediaItem mediaItem)

public voidonSeekCompleted()

public voidonSubtitleData(MediaItem mediaItem, int trackIndex, SubtitleData subtitleData)

public voidonTimedMetadata(MediaItem mediaItem, TimedMetaData timedMetaData)

public voidonVideoRenderingStart(MediaItem mediaItem)

public voidonVideoSizeChanged(MediaItem mediaItem, int width, int height)

public java.lang.Objectpause()

public java.lang.Objectplay()

public java.lang.Objectprepare()

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

public byte[]provideDrmKeyResponse(byte[] keySetId[], byte[] response[])

public voidreleaseDrm()

public voidreset()

public voidrestoreDrmKeys(byte[] keySetId[])

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

public java.lang.ObjectselectTrack(int index)

public java.lang.ObjectsetAudioAttributes(AudioAttributesCompat attributes)

public java.lang.ObjectsetAudioSessionId(int sessionId)

public java.lang.ObjectsetAuxEffectSendLevel(float auxEffectSendLevel)

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

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

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

public java.lang.ObjectsetMediaItem(MediaItem item)

public java.lang.ObjectsetNextMediaItem(MediaItem item)

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

public voidsetOnDrmConfigHelper(OnDrmConfigHelper listener)

public java.lang.ObjectsetPlaybackParams(PlaybackParams params)

public java.lang.ObjectsetPlayerVolume(float volume)

public java.lang.ObjectsetSurface(Surface surface)

public java.lang.ObjectskipToNext()

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

Constructors

public ExoPlayerMediaPlayer2Impl(Context context)

Creates a new ExoPlayer wrapper using the specified context.

Methods

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

public void clearPendingCommands()

public boolean cancel(java.lang.Object token)

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

public void clearEventCallback()

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

public void clearDrmEventCallback()

public java.lang.Object setAudioSessionId(int sessionId)

public java.lang.Object setMediaItem(MediaItem item)

public MediaItem getCurrentMediaItem()

public java.lang.Object prepare()

public java.lang.Object play()

public java.lang.Object pause()

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

public long getCurrentPosition()

public long getDuration()

public long getBufferedPosition()

public int getState()

public java.lang.Object loopCurrent(boolean loop)

public java.lang.Object skipToNext()

public java.lang.Object setNextMediaItem(MediaItem item)

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

public java.lang.Object setAudioAttributes(AudioAttributesCompat attributes)

public AudioAttributesCompat getAudioAttributes()

public int getAudioSessionId()

public java.lang.Object attachAuxEffect(int effectId)

public java.lang.Object setAuxEffectSendLevel(float auxEffectSendLevel)

public java.lang.Object setPlaybackParams(PlaybackParams params)

public PlaybackParams getPlaybackParams()

public int getVideoWidth()

public int getVideoHeight()

public java.lang.Object setSurface(Surface surface)

public java.lang.Object setPlayerVolume(float volume)

public float getPlayerVolume()

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

public int getSelectedTrack(int trackType)

public java.lang.Object selectTrack(int index)

public java.lang.Object deselectTrack(int index)

public PersistableBundle getMetrics()

public MediaTimestamp getTimestamp()

public void reset()

public void close()

public void setOnDrmConfigHelper(OnDrmConfigHelper listener)

public DrmInfo getDrmInfo()

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

public void releaseDrm()

public MediaDrm.KeyRequest getDrmKeyRequest(byte[] keySetId[], byte[] initData[], java.lang.String mimeType, int keyType, java.util.Map<java.lang.String, java.lang.String> optionalParameters)

public byte[] provideDrmKeyResponse(byte[] keySetId[], byte[] response[])

public void restoreDrmKeys(byte[] keySetId[])

public java.lang.String getDrmPropertyString(java.lang.String propertyName)

public void setDrmPropertyString(java.lang.String propertyName, java.lang.String value)

public void onPrepared(MediaItem mediaItem)

public void onMetadataChanged(MediaItem mediaItem)

public void onSeekCompleted()

public void onBufferingStarted(MediaItem mediaItem)

public void onBufferingEnded(MediaItem mediaItem)

public void onBufferingUpdate(MediaItem mediaItem, int bufferingPercentage)

public void onBandwidthSample(MediaItem mediaItem, int bitrateKbps)

public void onVideoRenderingStart(MediaItem mediaItem)

public void onVideoSizeChanged(MediaItem mediaItem, int width, int height)

public void onSubtitleData(MediaItem mediaItem, int trackIndex, SubtitleData subtitleData)

public void onTimedMetadata(MediaItem mediaItem, TimedMetaData timedMetaData)

public void onMediaItemStartedAsNext(MediaItem mediaItem)

public void onMediaItemEnded(MediaItem mediaItem)

public void onLoop(MediaItem mediaItem)

public void onMediaTimeDiscontinuity(MediaItem mediaItem, MediaTimestamp mediaTimestamp)

public void onPlaybackEnded(MediaItem mediaItem)

public void onError(MediaItem mediaItem, int what)

Source

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media2.player.exoplayer;

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

import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaDrm;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PersistableBundle;
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.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.SubtitleData;
import androidx.media2.exoplayer.external.Player;
import androidx.media2.player.MediaPlayer2;
import androidx.media2.player.MediaTimestamp;
import androidx.media2.player.PlaybackParams;
import androidx.media2.player.TimedMetaData;
import androidx.media2.player.futures.ResolvableFuture;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;

/**
 * An implementation of {@link MediaPlayer2} based on a repackaged version of ExoPlayer.
 *
 * @hide
 */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@SuppressLint("RestrictedApi") // TODO(b/68398926): Remove once RestrictedApi checks are fixed.
public final class ExoPlayerMediaPlayer2Impl extends MediaPlayer2
        implements ExoPlayerWrapper.Listener {

    private static final String TAG = "ExoPlayerMediaPlayer2";

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final ExoPlayerWrapper mPlayer;

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

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mLock;
    @GuardedBy("mLock")
    private Pair<Executor, EventCallback> mExecutorAndEventCallback;
    @SuppressWarnings("unused")
    @GuardedBy("mLock")
    private Pair<Executor, DrmEventCallback> mExecutorAndDrmEventCallback;
    @GuardedBy("mLock")
    private HandlerThread mHandlerThread;

    /** Creates a new ExoPlayer wrapper using the specified context. */
    public ExoPlayerMediaPlayer2Impl(@NonNull Context context) {
        mHandlerThread = new HandlerThread("ExoMediaPlayer2Thread");
        mHandlerThread.start();
        mPlayer = new ExoPlayerWrapper(
                context.getApplicationContext(),
                /* listener= */ this,
                mHandlerThread.getLooper());
        // Player callbacks will be called on the task handler thread.
        mTaskHandler = new Handler(mPlayer.getLooper());
        mPendingTasks = new ArrayDeque<>();
        mTaskLock = new Object();
        mLock = new Object();
        resetPlayer();
    }

    // Command queue and events implementation.

    @Override
    public Object notifyWhenCommandLabelReached(@NonNull 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(ExoPlayerMediaPlayer2Impl.this, label);
                    }
                });
            }
        });
    }

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

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

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

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

    @Override
    public void setEventCallback(@NonNull Executor executor, @NonNull EventCallback eventCallback) {
        Preconditions.checkNotNull(executor);
        Preconditions.checkNotNull(eventCallback);
        synchronized (mLock) {
            mExecutorAndEventCallback = Pair.create(executor, eventCallback);
        }
    }

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

    @Override
    public void setDrmEventCallback(@NonNull Executor executor,
            @NonNull DrmEventCallback eventCallback) {
        Preconditions.checkNotNull(executor);
        Preconditions.checkNotNull(eventCallback);
        synchronized (mLock) {
            mExecutorAndDrmEventCallback = Pair.create(executor, eventCallback);
        }
    }

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

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

    // Player implementation.

    @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 Object setMediaItem(@NonNull final MediaItem item) {
        return addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
            @Override
            void process() {
                mPlayer.setMediaItem(item);
            }
        });
    }

    @Override
    public MediaItem getCurrentMediaItem() {
        return runPlayerCallableBlocking(new Callable<MediaItem>() {
            @Override
            public MediaItem call() throws Exception {
                return mPlayer.getCurrentMediaItem();
            }
        });
    }

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

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

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

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

    @Override
    public long getCurrentPosition() {
        return runPlayerCallableBlocking(new Callable<Long>() {
            @Override
            public Long call() throws Exception {
                return mPlayer.getCurrentPosition();
            }
        });
    }

    @Override
    public long getDuration() {
        return runPlayerCallableBlocking(new Callable<Long>() {
            @Override
            public Long call() throws Exception {
                return mPlayer.getDuration();
            }
        });
    }

    @Override
    public long getBufferedPosition() {
        return runPlayerCallableBlocking(new Callable<Long>() {
            @Override
            public Long call() throws Exception {
                return mPlayer.getBufferedPosition();
            }
        });
    }

    @Override
    public @MediaPlayer2.MediaPlayer2State int getState() {
        return runPlayerCallableBlocking(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return mPlayer.getState();
            }
        });
    }

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

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

    @Override
    public Object setNextMediaItem(@NonNull final MediaItem item) {
        return addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
            @Override
            void process() {
                mPlayer.setNextMediaItem(item);
            }
        });
    }

    @Override
    public Object setNextMediaItems(@NonNull final List<MediaItem> items) {
        return addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
            @Override
            void process() {
                mPlayer.setNextMediaItems(items);
            }
        });
    }

    @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 AudioAttributesCompat getAudioAttributes() {
        return runPlayerCallableBlocking(new Callable<AudioAttributesCompat>() {
            @Override
            public AudioAttributesCompat call() throws Exception {
                return mPlayer.getAudioAttributes();
            }
        });
    }

    @Override
    public int getAudioSessionId() {
        return runPlayerCallableBlocking(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                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 auxEffectSendLevel) {
        return addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
            @Override
            void process() {
                mPlayer.setAuxEffectSendLevel(auxEffectSendLevel);
            }
        });
    }

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

    @Override
    @NonNull
    public PlaybackParams getPlaybackParams() {
        return runPlayerCallableBlocking(new Callable<PlaybackParams>() {
            @Override
            public PlaybackParams call() throws Exception {
                return mPlayer.getPlaybackParams();
            }
        });
    }

    @Override
    public int getVideoWidth() {
        return runPlayerCallableBlocking(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return mPlayer.getVideoWidth();
            }
        });
    }

    @Override
    public int getVideoHeight() {
        return runPlayerCallableBlocking(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return mPlayer.getVideoHeight();
            }
        });
    }

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

    @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 runPlayerCallableBlocking(new Callable<Float>() {
            @Override
            public Float call() throws Exception {
                return mPlayer.getVolume();
            }
        });
    }

    @Override
    public List<TrackInfo> getTrackInfo() {
        return runPlayerCallableBlocking(new Callable<List<TrackInfo>>() {
            @Override
            public List<TrackInfo> call() throws Exception {
                return mPlayer.getTrackInfo();
            }
        });
    }
    @Override
    public int getSelectedTrack(final int trackType) {
        return runPlayerCallableBlocking(new Callable<Integer>() {
            @Override
            public Integer call() {
                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
    @RequiresApi(21)
    public PersistableBundle getMetrics() {
        return runPlayerCallableBlocking(new Callable<PersistableBundle>() {
            @Override
            public PersistableBundle call() throws Exception {
                return mPlayer.getMetricsV21();
            }
        });
    }

    @Override
    public MediaTimestamp getTimestamp() {
        return runPlayerCallableBlocking(new Callable<MediaTimestamp>() {
            @Override
            public MediaTimestamp call() {
                return mPlayer.getTimestamp();
            }
        });
    }

    @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) {
                    // Suppress interruption.
                }
            }
        }
        mTaskHandler.removeCallbacksAndMessages(null);
        runPlayerCallableBlocking(new Callable<Void>() {
            @Override
            public Void call() {
                mPlayer.reset();
                return null;
            }
        });
    }

    @Override
    public void close() {
        clearEventCallback();
        HandlerThread handlerThread;
        synchronized (mLock) {
            handlerThread = mHandlerThread;
            if (handlerThread == null) {
                return;
            }
            mHandlerThread = null;
        }
        runPlayerCallableBlocking(new Callable<Void>() {
            @Override
            public Void call() {
                mPlayer.close();
                return null;
            }
        });
        handlerThread.quit();
    }

    @Override
    public void setOnDrmConfigHelper(OnDrmConfigHelper listener) {
        throw new UnsupportedOperationException();
    }

    @Override
    public DrmInfo getDrmInfo() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object prepareDrm(@NonNull final UUID uuid) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void releaseDrm() {
        throw new UnsupportedOperationException();
    }

    @Override
    @NonNull
    public MediaDrm.KeyRequest getDrmKeyRequest(byte[] keySetId, byte[] initData, String mimeType,
            int keyType, Map<String, String> optionalParameters) {
        throw new UnsupportedOperationException();
    }

    @Override
    public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void restoreDrmKeys(@NonNull byte[] keySetId) {
        throw new UnsupportedOperationException();
    }

    @Override
    @NonNull
    public String getDrmPropertyString(@NonNull String propertyName) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void setDrmPropertyString(@NonNull String propertyName, @NonNull String value) {
        throw new UnsupportedOperationException();
    }

    // ExoPlayerWrapper.Listener implementation.

    @Override
    public void onPrepared(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_PREPARED);
        synchronized (mTaskLock) {
            if (mCurrentTask != null
                    && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
                    && ObjectsCompat.equals(mCurrentTask.mDSD, mediaItem)
                    && mCurrentTask.mNeedToWaitForEventToComplete) {
                mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
                mCurrentTask = null;
                processPendingTask();
            }
        }
    }

    @Override
    public void onMetadataChanged(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_METADATA_UPDATE);
    }

    @Override
    public void onSeekCompleted() {
        synchronized (mTaskLock) {
            if (mCurrentTask != null
                    && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
                    && mCurrentTask.mNeedToWaitForEventToComplete) {
                mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
                mCurrentTask = null;
                processPendingTask();
            }
        }
    }

    @Override
    public void onBufferingStarted(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_BUFFERING_START);
    }

    @Override
    public void onBufferingEnded(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_BUFFERING_END);
    }

    @Override
    public void onBufferingUpdate(MediaItem mediaItem, int bufferingPercentage) {
        notifyOnInfo(mediaItem, MEDIA_INFO_BUFFERING_UPDATE, bufferingPercentage);
    }

    @Override
    public void onBandwidthSample(MediaItem mediaItem, int bitrateKbps) {
        notifyOnInfo(mediaItem, MEDIA_INFO_NETWORK_BANDWIDTH, bitrateKbps);
    }

    @Override
    public void onVideoRenderingStart(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_VIDEO_RENDERING_START);
    }

    @Override
    public void onVideoSizeChanged(final MediaItem mediaItem, final int width, final int height) {
        notifyMediaPlayer2Event(new ExoPlayerMediaPlayer2Impl.Mp2EventNotifier() {
            @Override
            public void notify(MediaPlayer2.EventCallback callback) {
                callback.onVideoSizeChanged(
                        ExoPlayerMediaPlayer2Impl.this,
                        mediaItem,
                        width,
                        height);
            }
        });
    }

    @Override
    public void onSubtitleData(final MediaItem mediaItem, final int trackIndex,
            final SubtitleData subtitleData) {
        notifyMediaPlayer2Event(new Mp2EventNotifier() {
            @Override
            public void notify(EventCallback cb) {
                cb.onSubtitleData(
                        ExoPlayerMediaPlayer2Impl.this, mediaItem, trackIndex, subtitleData);
            }
        });
    }

    @Override
    public void onTimedMetadata(final MediaItem mediaItem, final TimedMetaData timedMetaData) {
        notifyMediaPlayer2Event(new Mp2EventNotifier() {
            @Override
            public void notify(EventCallback cb) {
                cb.onTimedMetaDataAvailable(
                        ExoPlayerMediaPlayer2Impl.this, mediaItem, timedMetaData);
            }
        });
    }

    @Override
    public void onMediaItemStartedAsNext(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_DATA_SOURCE_START);
    }

    @Override
    public void onMediaItemEnded(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_DATA_SOURCE_END);
    }

    @Override
    public void onLoop(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_DATA_SOURCE_REPEAT);
    }

    @Override
    public void onMediaTimeDiscontinuity(
            final MediaItem mediaItem, final MediaTimestamp mediaTimestamp) {
        notifyMediaPlayer2Event(new Mp2EventNotifier() {
            @Override
            public void notify(EventCallback cb) {
                cb.onMediaTimeDiscontinuity(
                        ExoPlayerMediaPlayer2Impl.this, mediaItem, mediaTimestamp);
            }
        });
    }

    @Override
    public void onPlaybackEnded(MediaItem mediaItem) {
        notifyOnInfo(mediaItem, MEDIA_INFO_DATA_SOURCE_LIST_END);
    }

    @Override
    public void onError(final MediaItem mediaItem, final int what) {
        synchronized (mTaskLock) {
            if (mCurrentTask != null
                    && mCurrentTask.mNeedToWaitForEventToComplete) {
                mCurrentTask.sendCompleteNotification(CALL_STATUS_ERROR_UNKNOWN);
                mCurrentTask = null;
                processPendingTask();
            }
        }
        notifyMediaPlayer2Event(new Mp2EventNotifier() {
            @Override
            public void notify(EventCallback cb) {
                cb.onError(ExoPlayerMediaPlayer2Impl.this, mediaItem, what, /* extra= */ 0);
            }
        });
    }

    // Internal functionality.

    private void notifyOnInfo(MediaItem mediaItem, int what) {
        notifyOnInfo(mediaItem, what, /* extra= */ 0);
    }

    private void notifyOnInfo(final MediaItem mediaItem, final int what, final int extra) {
        notifyMediaPlayer2Event(new ExoPlayerMediaPlayer2Impl.Mp2EventNotifier() {
            @Override
            public void notify(MediaPlayer2.EventCallback callback) {
                callback.onInfo(ExoPlayerMediaPlayer2Impl.this, mediaItem, what, extra);
            }
        });
    }

    private void resetPlayer() {
        runPlayerCallableBlocking(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                mPlayer.reset();
                return null;
            }
        });
    }

    /**
     * Runs the specified callable on the player thread, blocking the calling thread until a result
     * is returned.
     *
     * <p>Note: ExoPlayer methods apart from {@link Player#release} are asynchronous, so calling
     * player methods will not block the caller thread for a substantial amount of time.
     */
    private <T> T runPlayerCallableBlocking(final Callable<T> callable) {
        final ResolvableFuture<T> future = ResolvableFuture.create();
        boolean success = mTaskHandler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    future.set(callable.call());
                } catch (Throwable e) {
                    future.setException(e);
                }
            }
        });
        Preconditions.checkState(success);
        try {
            T result;
            boolean wasInterrupted = false;
            while (true) {
                try {
                    result = future.get();
                    break;
                } catch (InterruptedException e) {
                    // We always wait for player calls to return.
                    wasInterrupted = true;
                }
            }
            if (wasInterrupted) {
                Thread.currentThread().interrupt();
            }
            return result;
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            Log.e(TAG, "Internal player error", new RuntimeException(cause));
            throw new IllegalStateException(cause);
        }
    }

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

    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
                            && mPlayer.hasError()) {
                        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 = mPlayer.getCurrentMediaItem();

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

                synchronized (mTaskLock) {
                    mCurrentTask = null;
                    processPendingTask();
                }
            }
            // reset() might be waiting for this task. Notify that the task is done.
            synchronized (this) {
                mDone = true;
                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 process().
                return;
            }
            notifyMediaPlayer2Event(new Mp2EventNotifier() {
                @Override
                public void notify(EventCallback callback) {
                    callback.onCallCompleted(
                            ExoPlayerMediaPlayer2Impl.this, mDSD, mMediaCallType, status);
                }
            });
        }
    }

}