com.devbrackets.android.exomedia.core.exoplayer.ExoMediaPlayer.java Source code

Java tutorial

Introduction

Here is the source code for com.devbrackets.android.exomedia.core.exoplayer.ExoMediaPlayer.java

Source

/*
 * Copyright (C) 2015-2017 Brian Wernick,
 * Copyright (C) 2015 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 com.devbrackets.android.exomedia.core.exoplayer;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.PowerManager;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.support.v4.util.ArrayMap;
import android.util.Log;
import android.view.Surface;

import com.devbrackets.android.exomedia.ExoMedia;
import com.devbrackets.android.exomedia.ExoMedia.RendererType;
import com.devbrackets.android.exomedia.core.listener.CaptionListener;
import com.devbrackets.android.exomedia.core.listener.ExoPlayerListener;
import com.devbrackets.android.exomedia.core.listener.InternalErrorListener;
import com.devbrackets.android.exomedia.core.listener.MetadataListener;
import com.devbrackets.android.exomedia.core.renderer.RendererProvider;
import com.devbrackets.android.exomedia.core.source.MediaSourceProvider;
import com.devbrackets.android.exomedia.listener.OnBufferUpdateListener;
import com.devbrackets.android.exomedia.listener.OnLoopListener;
import com.devbrackets.android.exomedia.util.Repeater;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaDrm;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.MediaDrmCallback;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.video.VideoRendererEventListener;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

@SuppressWarnings({ "unused", "WeakerAccess" })
public class ExoMediaPlayer implements ExoPlayer.EventListener {
    private static final String TAG = "ExoMediaPlayer";
    private static final int BUFFER_REPEAT_DELAY = 1_000;
    private static final int WAKE_LOCK_TIMEOUT = 1_000;

    @NonNull
    private final Context context;
    @NonNull
    private final ExoPlayer player;
    @NonNull
    private final DefaultTrackSelector trackSelector;
    @NonNull
    private final AdaptiveTrackSelection.Factory adaptiveTrackSelectionFactory;
    @NonNull
    private final Handler mainHandler;
    @NonNull
    private final CopyOnWriteArrayList<ExoPlayerListener> listeners = new CopyOnWriteArrayList<>();

    @NonNull
    private final AtomicBoolean stopped = new AtomicBoolean();
    private boolean prepared = false;

    @NonNull
    private StateStore stateStore = new StateStore();
    @NonNull
    private Repeater bufferRepeater = new Repeater();

    @Nullable
    private Surface surface;
    @Nullable
    private MediaDrmCallback drmCallback;
    @Nullable
    private OnLoopListener onLoopListener;
    @Nullable
    private MediaSource mediaSource;
    @NonNull
    private List<Renderer> renderers = new LinkedList<>();
    @NonNull
    private MediaSourceProvider mediaSourceProvider = new MediaSourceProvider();
    @NonNull
    private DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();

    @Nullable
    private CaptionListener captionListener;
    @Nullable
    private MetadataListener metadataListener;
    @Nullable
    private InternalErrorListener internalErrorListener;
    @Nullable
    private OnBufferUpdateListener bufferUpdateListener;

    @Nullable
    private PowerManager.WakeLock wakeLock = null;

    @NonNull
    private CapabilitiesListener capabilitiesListener = new CapabilitiesListener();
    private int audioSessionId = C.AUDIO_SESSION_ID_UNSET;
    private MediaClock mediaClock;

    public ExoMediaPlayer(@NonNull Context context) {
        this.context = context;

        bufferRepeater.setRepeaterDelay(BUFFER_REPEAT_DELAY);
        bufferRepeater.setRepeatListener(new BufferRepeatListener());

        mainHandler = new Handler();

        ComponentListener componentListener = new ComponentListener();
        RendererProvider rendererProvider = new RendererProvider(context, mainHandler, componentListener,
                componentListener, componentListener, componentListener);
        rendererProvider.setDrmSessionManager(generateDrmSessionManager());

        renderers = rendererProvider.generate();

        adaptiveTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
        trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);

        LoadControl loadControl = ExoMedia.Data.loadControl != null ? ExoMedia.Data.loadControl
                : new DefaultLoadControl();
        player = ExoPlayerFactory.newInstance(renderers.toArray(new Renderer[renderers.size()]), trackSelector,
                loadControl);
        player.addListener(this);
    }

    @Override
    public void onTimelineChanged(Timeline timeline, Object manifest) {
        // Purposefully left blank
    }

    @Override
    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
        if (onLoopListener != null) {
            onLoopListener.onLoop();
        }
    }

    @Override
    public void onLoadingChanged(boolean isLoading) {
        // Purposefully left blank
    }

    @Override
    public void onPositionDiscontinuity() {
        // Purposefully left blank
    }

    @Override
    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
        // Purposefully left blank
    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int state) {
        reportPlayerState();
    }

    @Override
    public void onPlayerError(ExoPlaybackException exception) {
        for (ExoPlayerListener listener : listeners) {
            listener.onError(this, exception);
        }
    }

    /**
     * Sets the {@link MediaDrmCallback} to use when handling DRM for media.
     * This should be called before specifying the videos uri or path
     * <br>
     * <b>NOTE:</b> DRM is only supported on API 18 +
     *
     * @param drmCallback The callback to use when handling DRM media
     */
    public void setDrmCallback(@Nullable MediaDrmCallback drmCallback) {
        this.drmCallback = drmCallback;
    }

    public void setOnLoopListener(@Nullable OnLoopListener onLoopListener) {
        this.onLoopListener = onLoopListener;
    }

    public void setUri(@Nullable Uri uri, boolean loop) {
        MediaSource mediaSource = uri != null
                ? mediaSourceProvider.generate(context, mainHandler, uri, bandwidthMeter)
                : null;
        if (loop) {
            setMediaSource(new LoopingMediaSource(mediaSource));
        } else {
            setMediaSource(mediaSource);
        }
    }

    public void setMediaSource(@Nullable MediaSource source) {
        this.mediaSource = source;

        prepared = false;
        prepare();
    }

    public void addListener(ExoPlayerListener listener) {
        if (listener != null) {
            listeners.add(listener);
        }
    }

    public void removeListener(ExoPlayerListener listener) {
        if (listener != null) {
            listeners.remove(listener);
        }
    }

    public void setBufferUpdateListener(@Nullable OnBufferUpdateListener listener) {
        this.bufferUpdateListener = listener;
        setBufferRepeaterStarted(listener != null);
    }

    public void setInternalErrorListener(@Nullable InternalErrorListener listener) {
        internalErrorListener = listener;
    }

    public void setCaptionListener(@Nullable CaptionListener listener) {
        captionListener = listener;
    }

    public void setMetadataListener(@Nullable MetadataListener listener) {
        metadataListener = listener;
    }

    public void setSurface(@Nullable Surface surface) {
        this.surface = surface;
        sendMessage(C.TRACK_TYPE_VIDEO, C.MSG_SET_SURFACE, surface, false);
    }

    @Nullable
    public Surface getSurface() {
        return surface;
    }

    public void blockingClearSurface() {
        if (surface != null) {
            surface.release();
        }

        surface = null;
        sendMessage(C.TRACK_TYPE_VIDEO, C.MSG_SET_SURFACE, null, true);
    }

    /**
     * Retrieves a list of available tracks
     *
     * @return A list of available tracks associated with each type
     */
    @Nullable
    public Map<RendererType, TrackGroupArray> getAvailableTracks() {
        if (getPlaybackState() == ExoPlayer.STATE_IDLE) {
            return null;
        }

        // Retrieves the available tracks
        Map<RendererType, TrackGroupArray> trackMap = new ArrayMap<>();
        MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
        if (mappedTrackInfo == null) {
            return trackMap;
        }

        // Maps the available tracks
        RendererType[] types = new RendererType[] { RendererType.AUDIO, RendererType.VIDEO,
                RendererType.CLOSED_CAPTION, RendererType.METADATA };
        for (RendererType type : types) {
            int exoPlayerTrackIndex = getExoPlayerTrackType(type);
            if (mappedTrackInfo.length > exoPlayerTrackIndex) {
                trackMap.put(type, mappedTrackInfo.getTrackGroups(exoPlayerTrackIndex));
            }
        }

        return trackMap;
    }

    public int getSelectedTrackIndex(@NonNull RendererType type) {
        // Retrieves the available tracks
        int exoPlayerTrackIndex = getExoPlayerTrackType(type);
        MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
        TrackGroupArray trackGroupArray = mappedTrackInfo == null ? null
                : mappedTrackInfo.getTrackGroups(exoPlayerTrackIndex);
        if (trackGroupArray == null || trackGroupArray.length == 0) {
            return -1;
        }

        // Verifies the track selection has been overridden
        MappingTrackSelector.SelectionOverride selectionOverride = trackSelector
                .getSelectionOverride(exoPlayerTrackIndex, trackGroupArray);
        if (selectionOverride == null || selectionOverride.groupIndex != exoPlayerTrackIndex
                || selectionOverride.length <= 0) {
            return -1;
        }

        return selectionOverride.tracks[0];
    }

    public void setSelectedTrack(@NonNull RendererType type, int index) {
        // Retrieves the available tracks
        int exoPlayerTrackIndex = getExoPlayerTrackType(type);
        MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
        TrackGroupArray trackGroupArray = mappedTrackInfo == null ? null
                : mappedTrackInfo.getTrackGroups(exoPlayerTrackIndex);
        if (trackGroupArray == null || trackGroupArray.length == 0) {
            return;
        }

        // Creates the track selection override
        int[] tracks = new int[] { index };
        TrackSelection.Factory factory = tracks.length == 1 ? new FixedTrackSelection.Factory()
                : adaptiveTrackSelectionFactory;
        MappingTrackSelector.SelectionOverride selectionOverride = new MappingTrackSelector.SelectionOverride(
                factory, exoPlayerTrackIndex, tracks);

        // Specifies the correct track to use
        trackSelector.setSelectionOverride(exoPlayerTrackIndex, trackGroupArray, selectionOverride);
    }

    public void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {
        sendMessage(C.TRACK_TYPE_AUDIO, C.MSG_SET_VOLUME, volume);
    }

    public void forcePrepare() {
        prepared = false;
    }

    public void prepare() {
        if (prepared || mediaSource == null) {
            return;
        }

        if (!renderers.isEmpty()) {
            player.stop();
        }

        stateStore.reset();
        player.prepare(mediaSource);
        prepared = true;

        stopped.set(false);
    }

    public void stop() {
        if (!stopped.getAndSet(true)) {
            player.setPlayWhenReady(false);
            player.stop();
        }
    }

    public void setPlayWhenReady(boolean playWhenReady) {
        player.setPlayWhenReady(playWhenReady);
        stayAwake(playWhenReady);
    }

    public void seekTo(long positionMs) {
        player.seekTo(positionMs);
        stateStore.setMostRecentState(stateStore.isLastReportedPlayWhenReady(), StateStore.STATE_SEEKING);
    }

    /**
     * Seeks to the beginning of the media, and plays it. This method will not succeed if playback state is not {@code ExoPlayer.STATE_IDLE} or {@code ExoPlayer.STATE_ENDED}.
     *
     * @return {@code true} if the media was successfully restarted, otherwise {@code false}
     */
    public boolean restart() {
        int playbackState = getPlaybackState();
        if (playbackState != ExoPlayer.STATE_IDLE && playbackState != ExoPlayer.STATE_ENDED) {
            return false;
        }

        seekTo(0);
        setPlayWhenReady(true);

        forcePrepare();
        prepare();

        return true;
    }

    public void release() {
        setBufferRepeaterStarted(false);
        listeners.clear();

        surface = null;
        player.release();
        stayAwake(false);
    }

    public int getPlaybackState() {
        return player.getPlaybackState();
    }

    public int getAudioSessionId() {
        return audioSessionId;
    }

    public boolean setPlaybackSpeed(float speed) {
        PlaybackParameters params = new PlaybackParameters(speed, 1.0f);
        player.setPlaybackParameters(params);

        return true;
    }

    public long getCurrentPosition() {
        return player.getCurrentPosition();
    }

    public long getDuration() {
        return player.getDuration();
    }

    public int getBufferedPercentage() {
        return player.getBufferedPercentage();
    }

    public boolean getPlayWhenReady() {
        return player.getPlayWhenReady();
    }

    /**
     * This function has the MediaPlayer access the low-level power manager
     * service to control the device's power usage while playing is occurring.
     * The parameter is a combination of {@link android.os.PowerManager} wake flags.
     * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK}
     * permission.
     * By default, no attempt is made to keep the device awake during playback.
     *
     * @param context the Context to use
     * @param mode the power/wake mode to set
     * @see android.os.PowerManager
     */
    public void setWakeMode(Context context, int mode) {
        boolean wasHeld = false;
        if (wakeLock != null) {
            if (wakeLock.isHeld()) {
                wasHeld = true;
                wakeLock.release();
            }

            wakeLock = null;
        }

        //Acquires the wakelock if we have permissions to
        if (context.getPackageManager().checkPermission(Manifest.permission.WAKE_LOCK,
                context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            wakeLock = pm.newWakeLock(mode | PowerManager.ON_AFTER_RELEASE, ExoMediaPlayer.class.getName());
            wakeLock.setReferenceCounted(false);
        } else {
            Log.w(TAG, "Unable to acquire WAKE_LOCK due to missing manifest permission");
        }

        stayAwake(wasHeld);
    }

    protected int getExoPlayerTrackType(@NonNull RendererType type) {
        switch (type) {
        case AUDIO:
            return C.TRACK_TYPE_AUDIO;
        case VIDEO:
            return C.TRACK_TYPE_VIDEO;
        case CLOSED_CAPTION:
            return C.TRACK_TYPE_TEXT;
        case METADATA:
            return C.TRACK_TYPE_METADATA;
        }

        return C.TRACK_TYPE_UNKNOWN;
    }

    protected void sendMessage(int renderType, int messageType, Object message) {
        sendMessage(renderType, messageType, message, false);
    }

    protected void sendMessage(int renderType, int messageType, Object message, boolean blocking) {
        if (renderers.isEmpty()) {
            return;
        }

        List<ExoPlayer.ExoPlayerMessage> messages = new ArrayList<>();
        for (Renderer renderer : renderers) {
            if (renderer.getTrackType() == renderType) {
                messages.add(new ExoPlayer.ExoPlayerMessage(renderer, messageType, message));
            }
        }

        if (blocking) {
            player.blockingSendMessages(messages.toArray(new ExoPlayer.ExoPlayerMessage[messages.size()]));
        } else {
            player.sendMessages(messages.toArray(new ExoPlayer.ExoPlayerMessage[messages.size()]));
        }
    }

    /**
     * Used with playback state changes to correctly acquire and
     * release the wakelock if the user has enabled it with {@link #setWakeMode(Context, int)}.
     * If the {@link #wakeLock} is null then no action will be performed.
     *
     * @param awake True if the wakelock should be acquired
     */
    protected void stayAwake(boolean awake) {
        if (wakeLock == null) {
            return;
        }

        if (awake && !wakeLock.isHeld()) {
            wakeLock.acquire(WAKE_LOCK_TIMEOUT);
        } else if (!awake && wakeLock.isHeld()) {
            wakeLock.release();
        }
    }

    /**
     * Generates the {@link DrmSessionManager} to use with the {@link RendererProvider}. This will
     * return null on API's &lt; {@value Build.VERSION_CODES#JELLY_BEAN_MR2}
     *
     * @return The {@link DrmSessionManager} to use or <code>null</code>
     */
    @Nullable
    protected DrmSessionManager<FrameworkMediaCrypto> generateDrmSessionManager() {
        // DRM is only supported on API 18 + in the ExoPlayer
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return null;
        }

        // Widevine will capture the majority of use cases however playready is supported on all AndroidTV devices
        UUID uuid = C.WIDEVINE_UUID;

        try {
            return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid),
                    new DelegatedMediaDrmCallback(), null, mainHandler, capabilitiesListener);
        } catch (Exception e) {
            Log.d(TAG, "Unable to create a DrmSessionManager due to an exception", e);
            return null;
        }
    }

    private void reportPlayerState() {
        boolean playWhenReady = player.getPlayWhenReady();
        int playbackState = getPlaybackState();

        int newState = stateStore.getState(playWhenReady, playbackState);
        if (newState != stateStore.getMostRecentState()) {
            stateStore.setMostRecentState(playWhenReady, playbackState);

            //Makes sure the buffering notifications are sent
            if (newState == ExoPlayer.STATE_READY) {
                setBufferRepeaterStarted(true);
            } else if (newState == ExoPlayer.STATE_IDLE || newState == ExoPlayer.STATE_ENDED) {
                setBufferRepeaterStarted(false);
            }

            //Because the playWhenReady isn't a state in itself, rather a flag to a state we will ignore informing of
            // see events when that is the only change.  Additionally, on some devices we get states ordered as
            // [seeking, ready, buffering, ready] while on others we get [seeking, buffering, ready]
            boolean informSeekCompletion = stateStore.matchesHistory(
                    new int[] { StateStore.STATE_SEEKING, ExoPlayer.STATE_BUFFERING, ExoPlayer.STATE_READY }, true);
            informSeekCompletion |= stateStore.matchesHistory(
                    new int[] { ExoPlayer.STATE_BUFFERING, StateStore.STATE_SEEKING, ExoPlayer.STATE_READY }, true);
            informSeekCompletion |= stateStore.matchesHistory(new int[] { StateStore.STATE_SEEKING,
                    ExoPlayer.STATE_READY, ExoPlayer.STATE_BUFFERING, ExoPlayer.STATE_READY }, true);

            for (ExoPlayerListener listener : listeners) {
                listener.onStateChanged(playWhenReady, playbackState);

                if (informSeekCompletion) {
                    listener.onSeekComplete();
                }
            }
        }
    }

    private void setBufferRepeaterStarted(boolean start) {
        if (start && bufferUpdateListener != null) {
            bufferRepeater.start();
        } else {
            bufferRepeater.stop();
        }
    }

    public void setMediaClock(MediaClock mediaClock) {
        player.setMediaClock(mediaClock);
    }

    private static class StateStore {
        public static final int FLAG_PLAY_WHEN_READY = 0xF0000000;
        public static final int STATE_SEEKING = 100;

        //We keep the last few states because that is all we need currently
        private int[] prevStates = new int[] { ExoPlayer.STATE_IDLE, ExoPlayer.STATE_IDLE, ExoPlayer.STATE_IDLE,
                ExoPlayer.STATE_IDLE };

        public void reset() {
            for (int i = 0; i < prevStates.length; i++) {
                prevStates[i] = ExoPlayer.STATE_IDLE;
            }
        }

        public void setMostRecentState(boolean playWhenReady, int state) {
            int newState = getState(playWhenReady, state);
            if (prevStates[3] == newState) {
                return;
            }

            prevStates[0] = prevStates[1];
            prevStates[1] = prevStates[2];
            prevStates[2] = prevStates[3];
            prevStates[3] = state;
        }

        public int getState(boolean playWhenReady, int state) {
            return state | (playWhenReady ? FLAG_PLAY_WHEN_READY : 0);
        }

        public int getMostRecentState() {
            return prevStates[3];
        }

        public boolean isLastReportedPlayWhenReady() {
            return (prevStates[3] & FLAG_PLAY_WHEN_READY) != 0;
        }

        public boolean matchesHistory(@Size(min = 1, max = 4) int[] states, boolean ignorePlayWhenReady) {
            boolean flag = true;
            int andFlag = ignorePlayWhenReady ? ~FLAG_PLAY_WHEN_READY : ~0x0;
            int startIndex = prevStates.length - states.length;

            for (int i = startIndex; i < prevStates.length; i++) {
                flag &= (prevStates[i] & andFlag) == (states[i - startIndex] & andFlag);
            }

            return flag;
        }
    }

    private class BufferRepeatListener implements Repeater.RepeatListener {
        @Override
        public void onRepeat() {
            if (bufferUpdateListener != null) {
                bufferUpdateListener.onBufferingUpdate(getBufferedPercentage());
            }
        }
    }

    /**
     * Delegates the {@link #drmCallback} so that we don't need to re-initialize the renderers
     * when the callback is set.
     */
    private class DelegatedMediaDrmCallback implements MediaDrmCallback {
        @Override
        public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request) throws Exception {
            return drmCallback != null ? drmCallback.executeProvisionRequest(uuid, request) : new byte[0];
        }

        @Override
        public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request) throws Exception {
            return drmCallback != null ? drmCallback.executeKeyRequest(uuid, request) : new byte[0];
        }
    }

    private class CapabilitiesListener implements DefaultDrmSessionManager.EventListener {
        @Override
        public void onDrmKeysLoaded() {
            // Purposefully left blank
        }

        @Override
        public void onDrmKeysRestored() {
            // Purposefully left blank
        }

        @Override
        public void onDrmKeysRemoved() {
            // Purposefully left blank
        }

        @Override
        public void onDrmSessionManagerError(Exception e) {
            if (internalErrorListener != null) {
                internalErrorListener.onDrmSessionManagerError(e);
            }
        }
    }

    private class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener,
            TextRenderer.Output, MetadataRenderer.Output {

        @Override
        public void onAudioEnabled(DecoderCounters counters) {
            // Purposefully left blank
        }

        @Override
        public void onAudioDisabled(DecoderCounters counters) {
            audioSessionId = C.AUDIO_SESSION_ID_UNSET;
        }

        @Override
        public void onAudioSessionId(int sessionId) {
            audioSessionId = sessionId;
        }

        @Override
        public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
                long initializationDurationMs) {
            // Purposefully left blank
        }

        @Override
        public void onAudioInputFormatChanged(Format format) {
            // Purposefully left blank
        }

        @Override
        public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
            if (internalErrorListener != null) {
                internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
            }
        }

        @Override
        public void onVideoEnabled(DecoderCounters counters) {
            // Purposefully left blank
        }

        @Override
        public void onVideoDisabled(DecoderCounters counters) {
            // Purposefully left blank
        }

        @Override
        public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
                long initializationDurationMs) {
            // Purposefully left blank
        }

        @Override
        public void onVideoInputFormatChanged(Format format) {
            // Purposefully left blank
        }

        @Override
        public void onDroppedFrames(int count, long elapsedMs) {
            // Purposefully left blank
        }

        @Override
        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
                float pixelWidthHeightRatio) {
            for (ExoPlayerListener listener : listeners) {
                listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
            }
        }

        @Override
        public void onRenderedFirstFrame(Surface surface) {
            // Purposefully left blank
        }

        @Override
        public void onMetadata(Metadata metadata) {
            if (metadataListener != null) {
                metadataListener.onMetadata(metadata);
            }
        }

        @Override
        public void onCues(List<Cue> cues) {
            if (captionListener != null) {
                captionListener.onCues(cues);
            }
        }
    }
}