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

Java tutorial

Introduction

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

Source

/*
 * Copyright (C) 2016 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.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
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.annotation.TrackRenderType;
import com.devbrackets.android.exomedia.core.builder.RenderBuilder;
import com.devbrackets.android.exomedia.core.listener.CaptionListener;
import com.devbrackets.android.exomedia.core.listener.ExoPlayerListener;
import com.devbrackets.android.exomedia.core.listener.Id3MetadataListener;
import com.devbrackets.android.exomedia.core.listener.InfoListener;
import com.devbrackets.android.exomedia.core.listener.InternalErrorListener;
import com.devbrackets.android.exomedia.core.renderer.EMMediaCodecAudioTrackRenderer;
import com.devbrackets.android.exomedia.listener.OnBufferUpdateListener;
import com.devbrackets.android.exomedia.util.Repeater;
import com.google.android.exoplayer.DummyTrackRenderer;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.metadata.id3.Id3Frame;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.TextRenderer;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

@SuppressWarnings("unused")
public class EMExoPlayer implements ExoPlayer.Listener, AudioCapabilitiesReceiver.Listener,
        ExtractorSampleSource.EventListener, ChunkSampleSource.EventListener, HlsSampleSource.EventListener,
        DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
        MediaCodecAudioTrackRenderer.EventListener, StreamingDrmSessionManager.EventListener,
        DashChunkSource.EventListener, MetadataTrackRenderer.MetadataRenderer<List<Id3Frame>>, TextRenderer {
    private static final String TAG = "EMExoPlayer";
    public static final int DISABLED_TRACK = -1;

    public static final int RENDER_COUNT = 4;
    public static final int RENDER_VIDEO = 0;
    public static final int RENDER_AUDIO = 1;
    public static final int RENDER_CLOSED_CAPTION = 2;
    public static final int RENDER_TIMED_METADATA = 3;

    public static final int BUFFER_LENGTH_MIN = 1000;
    public static final int REBUFFER_LENGTH_MIN = 5000;
    private static final int BUFFER_REPEAT_DELAY = 1000;

    public enum RenderBuildingState {
        IDLE, BUILDING, BUILT
    }

    private RenderBuilder rendererBuilder;
    private final ExoPlayer player;
    private final Handler mainHandler;
    private final CopyOnWriteArrayList<ExoPlayerListener> listeners;

    private final AtomicBoolean stopped = new AtomicBoolean();

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

    private boolean prepared = false;

    private Surface surface;
    private TrackRenderer videoRenderer;
    private TrackRenderer audioRenderer;

    @Nullable
    private AudioCapabilities audioCapabilities;
    @Nullable
    private AudioCapabilitiesReceiver audioCapabilitiesReceiver;

    @Nullable
    private CaptionListener captionListener;
    @Nullable
    private Id3MetadataListener id3MetadataListener;
    @Nullable
    private InternalErrorListener internalErrorListener;
    @Nullable
    private InfoListener infoListener;
    @Nullable
    private OnBufferUpdateListener bufferUpdateListener;

    @Nullable
    private PowerManager.WakeLock wakeLock = null;

    public EMExoPlayer() {
        this(null);
    }

    public EMExoPlayer(@Nullable RenderBuilder rendererBuilder) {
        bufferRepeater.setRepeaterDelay(BUFFER_REPEAT_DELAY);
        bufferRepeater.setRepeatListener(new BufferRepeatListener());

        player = ExoPlayer.Factory.newInstance(RENDER_COUNT, BUFFER_LENGTH_MIN, REBUFFER_LENGTH_MIN);
        player.addListener(this);

        mainHandler = new Handler();
        listeners = new CopyOnWriteArrayList<>();
        rendererBuildingState = RenderBuildingState.IDLE;
        player.setSelectedTrack(RENDER_CLOSED_CAPTION, DISABLED_TRACK);

        replaceRenderBuilder(rendererBuilder);
    }

    public void replaceRenderBuilder(@Nullable RenderBuilder renderBuilder) {
        this.rendererBuilder = renderBuilder;

        if (audioCapabilitiesReceiver != null) {
            audioCapabilitiesReceiver.unregister();
            audioCapabilitiesReceiver = null;
        }

        if (rendererBuilder != null && audioCapabilities == null) {
            audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(rendererBuilder.getContext(), this);
            audioCapabilitiesReceiver.register();
        }

        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 setInfoListener(@Nullable InfoListener listener) {
        infoListener = listener;
    }

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

    public void setMetadataListener(@Nullable Id3MetadataListener listener) {
        id3MetadataListener = listener;
    }

    public void setSurface(Surface surface) {
        this.surface = surface;
        pushSurface(false);
    }

    public Surface getSurface() {
        return surface;
    }

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

        surface = null;
        pushSurface(true);
    }

    @Nullable
    public AudioCapabilities getAudioCapabilities() {
        return audioCapabilities;
    }

    /**
     * Retrieves a list of available tracks
     *
     * @return A list of available tracks associated with each type (see {@link com.devbrackets.android.exomedia.annotation.TrackRenderType})
     */
    @Nullable
    public Map<Integer, List<MediaFormat>> getAvailableTracks() {
        if (getPlaybackState() == ExoPlayer.STATE_IDLE) {
            return null;
        }

        Map<Integer, List<MediaFormat>> trackMap = new ArrayMap<>();
        int[] trackTypes = new int[] { RENDER_AUDIO, RENDER_VIDEO, RENDER_CLOSED_CAPTION, RENDER_TIMED_METADATA };

        //Populates the map with all available tracks
        for (int type : trackTypes) {
            int trackCount = getTrackCount(type);
            List<MediaFormat> tracks = new ArrayList<>(trackCount);
            trackMap.put(type, tracks);

            for (int i = 0; i < trackCount; i++) {
                tracks.add(getTrackFormat(type, i));
            }
        }

        return trackMap;
    }

    public int getTrackCount(@TrackRenderType int type) {
        return player.getTrackCount(type);
    }

    public MediaFormat getTrackFormat(@TrackRenderType int type, int index) {
        return player.getTrackFormat(type, index);
    }

    public int getSelectedTrack(@TrackRenderType int type) {
        return player.getSelectedTrack(type);
    }

    public void setSelectedTrack(@TrackRenderType int type, int index) {
        player.setSelectedTrack(type, index);
        if (type == RENDER_CLOSED_CAPTION && index == DISABLED_TRACK && captionListener != null) {
            captionListener.onCues(Collections.<Cue>emptyList());
        }
    }

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

    public void forcePrepare() {
        prepared = false;
    }

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

        if (rendererBuildingState == RenderBuildingState.BUILT) {
            player.stop();
        }

        videoRenderer = null;
        rendererBuildingState = RenderBuildingState.BUILDING;
        reportPlayerState();

        rendererBuilder.buildRenderers(this);
        prepared = true;

        stopped.set(false);
    }

    public void onRenderers(TrackRenderer[] renderers, @Nullable BandwidthMeter bandwidthMeter) {
        for (int i = 0; i < RENDER_COUNT; i++) {
            if (renderers[i] == null) {
                // Convert a null renderer to a dummy renderer.
                renderers[i] = new DummyTrackRenderer();
            }
        }

        // Complete preparation.
        this.videoRenderer = renderers[RENDER_VIDEO];
        this.audioRenderer = renderers[RENDER_AUDIO];

        pushSurface(false);
        player.prepare(renderers);
        rendererBuildingState = RenderBuildingState.BUILT;
    }

    public void onRenderersError(Exception e) {
        if (internalErrorListener != null) {
            internalErrorListener.onRendererInitializationError(e);
        }

        for (ExoPlayerListener listener : listeners) {
            listener.onError(this, e);
        }

        rendererBuildingState = RenderBuildingState.IDLE;
        reportPlayerState();
    }

    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() {
        if (rendererBuilder != null) {
            rendererBuilder.cancel();
        }

        if (audioCapabilitiesReceiver != null) {
            audioCapabilitiesReceiver.unregister();
            audioCapabilitiesReceiver = null;
        }

        setBufferRepeaterStarted(false);
        rendererBuildingState = RenderBuildingState.IDLE;
        listeners.clear();

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

    public int getPlaybackState() {
        if (rendererBuildingState == RenderBuildingState.BUILDING) {
            return ExoPlayer.STATE_PREPARING;
        }

        return player.getPlaybackState();
    }

    public int getAudioSessionId() {
        if (audioRenderer != null) {
            return ((EMMediaCodecAudioTrackRenderer) audioRenderer).getAudioSessionId();
        }

        return 0;
    }

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

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

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

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

    public Looper getPlaybackLooper() {
        return player.getPlaybackLooper();
    }

    public Handler getMainHandler() {
        return mainHandler;
    }

    /**
     * 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, EMExoPlayer.class.getName());
            wakeLock.setReferenceCounted(false);
        } else {
            Log.w(TAG, "Unable to acquire WAKE_LOCK due to missing manifest permission");
        }

        stayAwake(wasHeld);
    }

    /**
     * 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();
        } else if (!awake && wakeLock.isHeld()) {
            wakeLock.release();
        }
    }

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

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

    @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 onDroppedFrames(int count, long elapsed) {
        if (infoListener != null) {
            infoListener.onDroppedFrames(count, elapsed);
        }
    }

    @Override
    public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
        if (infoListener != null) {
            infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
        }
    }

    @Override
    public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, long mediaTimeMs) {
        if (infoListener == null) {
            return;
        }

        if (sourceId == RENDER_VIDEO) {
            infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
        } else if (sourceId == RENDER_AUDIO) {
            infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
        }
    }

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

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

    @Override
    public void onDecoderInitializationError(MediaCodecTrackRenderer.DecoderInitializationException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onDecoderInitializationError(e);
        }
    }

    @Override
    public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) {
        if (infoListener != null) {
            infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
        }
    }

    @Override
    public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onAudioTrackInitializationError(e);
        }
    }

    @Override
    public void onAudioTrackWriteError(AudioTrack.WriteException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onAudioTrackWriteError(e);
        }
    }

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

    @Override
    public void onCryptoError(MediaCodec.CryptoException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onCryptoError(e);
        }
    }

    @Override
    public void onLoadError(int sourceId, IOException e) {
        if (internalErrorListener != null) {
            internalErrorListener.onLoadError(sourceId, e);
        }
    }

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

    @Override
    public void onMetadata(List<Id3Frame> metadata) {
        if (id3MetadataListener != null && getSelectedTrack(RENDER_TIMED_METADATA) != DISABLED_TRACK) {
            id3MetadataListener.onId3Metadata(metadata);
        }
    }

    @Override
    public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
        if (infoListener != null) {
            infoListener.onAvailableRangeChanged(sourceId, availableRange);
        }
    }

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

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

    @Override
    public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
            long mediaStartTimeMs, long mediaEndTimeMs) {
        if (infoListener != null) {
            infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs, mediaEndTimeMs);
        }
    }

    @Override
    public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
            long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
        if (infoListener != null) {
            infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
                    mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
        }
    }

    @Override
    public void onLoadCanceled(int sourceId, long bytesLoaded) {
        //Purposefully left blank
    }

    @Override
    public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
        //Purposefully left blank
    }

    @Override
    public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
        if (audioCapabilities.equals(this.audioCapabilities)) {
            return;
        }

        this.audioCapabilities = audioCapabilities;
        if (rendererBuilder == null) {
            return;
        }

        //Restarts the media playback to allow the RenderBuilder to handle the audio channel determination
        boolean playWhenReady = getPlayWhenReady();
        long currentPosition = getCurrentPosition();

        replaceRenderBuilder(rendererBuilder);

        player.seekTo(currentPosition);
        player.setPlayWhenReady(playWhenReady);
    }

    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
                    || newState == ExoPlayer.STATE_PREPARING) {
                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_READY }, true);
            informSeekCompletion |= stateStore.matchesHistory(
                    new int[] { StateStore.STATE_SEEKING, ExoPlayer.STATE_BUFFERING, 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 pushSurface(boolean blockForSurfacePush) {
        if (videoRenderer == null) {
            return;
        }

        if (blockForSurfacePush) {
            player.blockingSendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
        } else {
            player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
        }
    }

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

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

    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 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;
        }
    }
}