RoutePlayer2.java

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

package androidx.media.widget;

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

import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;

import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.media.AudioAttributesCompat;
import androidx.media2.DataSourceDesc2;
import androidx.media2.MediaPlayerConnector;
import androidx.media2.UriDataSourceDesc2;
import androidx.mediarouter.media.MediaItemStatus;
import androidx.mediarouter.media.MediaRouter;
import androidx.mediarouter.media.MediaSessionStatus;
import androidx.mediarouter.media.RemotePlaybackClient;
import androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback;
import androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback;
import androidx.mediarouter.media.RemotePlaybackClient.StatusCallback;

import java.util.List;
import java.util.concurrent.Executor;

/**
 * @hide
 */
@RestrictTo(LIBRARY_GROUP)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class RoutePlayer2 extends MediaPlayerConnector {
    private static final String TAG = "RoutePlayer2";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    String mItemId;
    int mCurrentPlayerState;
    long mDuration;
    long mLastStatusChangedTime;
    long mPosition;
    boolean mCanResume;
    ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
            new ArrayMap<>();
    private RemotePlaybackClient mClient;
    private DataSourceDesc2 mDsd;

    private StatusCallback mStatusCallback = new StatusCallback() {
        @Override
        public void onItemStatusChanged(Bundle data,
                String sessionId, MediaSessionStatus sessionStatus,
                String itemId, MediaItemStatus itemStatus) {
            if (DEBUG && !isSessionActive(sessionStatus)) {
                Log.v(TAG, "onItemStatusChanged() is called, but session is not active.");
            }
            mLastStatusChangedTime = SystemClock.elapsedRealtime();
            mPosition = itemStatus.getContentPosition();
            mCurrentPlayerState = convertPlaybackStateToPlayerState(itemStatus.getPlaybackState());
            if (mPlayerEventCallbackMap.size() > 0) {
                for (PlayerEventCallback callback : mPlayerEventCallbackMap.keySet()) {
                    callback.onPlayerStateChanged(RoutePlayer2.this, mCurrentPlayerState);
                }
            }
        }
    };

    public RoutePlayer2(Context context, MediaRouter.RouteInfo route) {
        mClient = new RemotePlaybackClient(context, route);
        mClient.setStatusCallback(mStatusCallback);
        if (mClient.isSessionManagementSupported()) {
            mClient.startSession(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "RoutePlayer2 has been initialized, but session is not"
                                + "active.");
                    }
                }
            });
        }
    }

    @Override
    public void play() {
        if (mDsd == null) {
            return;
        }

        // RemotePlaybackClient cannot call resume(..) without calling pause(..) first.
        if (!mCanResume) {
            playInternal();
            return;
        }

        if (mClient.isSessionManagementSupported()) {
            mClient.resume(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "play() is called, but session is not active.");
                    }
                    // Do nothing since this returns the buffering state--
                    // StatusCallback#onItemStatusChanged is called when the session reaches the
                    // play state.
                }
            });
        }
    }

    @Override
    public void prepare() {
        // Do nothing
    }

    @Override
    public void pause() {
        if (mClient.isSessionManagementSupported()) {
            mClient.pause(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "pause() is called, but session is not active.");
                    }
                    mCanResume = true;
                    // Do not update playback state here since this returns the buffering state--
                    // StatusCallback#onItemStatusChanged is called when the session reaches the
                    // pause state.
                }
            });
        }
    }

    @Override
    public void reset() {
        if (mClient.isSessionManagementSupported()) {
            mClient.stop(null, new SessionActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "reset() is called, but session is not active.");
                    }
                }
            });
        }
    }

    @Override
    public void skipToNext() {
        // TODO: implement
    }

    @Override
    public void seekTo(long pos) {
        if (mClient.isSessionManagementSupported()) {
            mClient.seek(mItemId, pos, null, new ItemActionCallback() {
                @Override
                public void onResult(Bundle data,
                        String sessionId, MediaSessionStatus sessionStatus,
                        String itemId, MediaItemStatus itemStatus) {
                    if (DEBUG && !isSessionActive(sessionStatus)) {
                        Log.v(TAG, "seekTo(long) is called, but session is not active.");
                    }
                    if (itemStatus != null) {
                        if (mPlayerEventCallbackMap.size() > 0) {
                            for (PlayerEventCallback callback : mPlayerEventCallbackMap.keySet()) {
                                callback.onSeekCompleted(RoutePlayer2.this,
                                        itemStatus.getContentPosition());
                            }
                        }
                    }
                }
            });
        }
    }

    @Override
    public long getCurrentPosition() {
        long expectedPosition = mPosition;
        if (mCurrentPlayerState == PLAYER_STATE_PLAYING) {
            expectedPosition = mPosition + (SystemClock.elapsedRealtime() - mLastStatusChangedTime);
        }
        return expectedPosition;
    }

    @Override
    public long getDuration() {
        return mDuration;
    }

    @Override
    public long getBufferedPosition() {
        return 0;
    }

    @Override
    public int getPlayerState() {
        return mCurrentPlayerState;
    }

    @Override
    public int getBufferingState() {
        return MediaPlayerConnector.BUFFERING_STATE_UNKNOWN;
    }

    @Override
    public void setAudioAttributes(AudioAttributesCompat attributes) {
        // TODO: implement
    }

    @Override
    public AudioAttributesCompat getAudioAttributes() {
        return null;
    }

    @Override
    public void setDataSource(DataSourceDesc2 dsd) {
        mDsd = dsd;
    }

    @Override
    public void setNextDataSource(DataSourceDesc2 dsd) {
        // TODO: implement
    }

    @Override
    public void setNextDataSources(List<DataSourceDesc2> dsds) {
        // TODO: implement
    }

    @Override
    public DataSourceDesc2 getCurrentDataSource() {
        return mDsd;
    }

    @Override
    public void loopCurrent(boolean loop) {
        // TODO: implement
    }

    @Override
    public void setPlaybackSpeed(float speed) {
        // Do nothing
    }

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

    @Override
    public void setPlayerVolume(float volume) {
        // TODO: implement
    }

    @Override
    public float getPlayerVolume() {
        return 0;
    }

    @Override
    public void registerPlayerEventCallback(
            Executor e, MediaPlayerConnector.PlayerEventCallback cb) {
        mPlayerEventCallbackMap.put(cb, e);
    }

    @Override
    public void unregisterPlayerEventCallback(MediaPlayerConnector.PlayerEventCallback cb) {
        mPlayerEventCallbackMap.remove(cb);
    }

    @Override
    public void close() {
        if (mClient != null) {
            try {
                mClient.release();
            } catch (IllegalArgumentException e) {
                Log.d(TAG, "Receiver not registered");
            }
            mClient = null;
        }
        mPlayerEventCallbackMap.clear();
    }

    void setCurrentPosition(long position) {
        mPosition = position;
    }

    boolean isSessionActive(MediaSessionStatus status) {
        if (status == null || status.getSessionState() == MediaSessionStatus.SESSION_STATE_ENDED
                || status.getSessionState() == MediaSessionStatus.SESSION_STATE_INVALIDATED) {
            return false;
        }
        return true;
    }

    int convertPlaybackStateToPlayerState(int playbackState) {
        int playerState = PLAYER_STATE_IDLE;
        switch (playbackState) {
            case MediaItemStatus.PLAYBACK_STATE_PENDING:
            case MediaItemStatus.PLAYBACK_STATE_FINISHED:
            case MediaItemStatus.PLAYBACK_STATE_CANCELED:
                playerState = PLAYER_STATE_IDLE;
                break;
            case MediaItemStatus.PLAYBACK_STATE_PLAYING:
                playerState = PLAYER_STATE_PLAYING;
                break;
            case MediaItemStatus.PLAYBACK_STATE_PAUSED:
            case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
                playerState = PLAYER_STATE_PAUSED;
                break;
            case MediaItemStatus.PLAYBACK_STATE_INVALIDATED:
            case MediaItemStatus.PLAYBACK_STATE_ERROR:
                playerState =  PLAYER_STATE_ERROR;
                break;
        }
        return playerState;
    }

    private void playInternal() {
        if (mDsd.getType() != DataSourceDesc2.TYPE_URI) {
            Log.w(TAG, "Data source type is not Uri." + mDsd);
            return;
        }
        mClient.play(((UriDataSourceDesc2) mDsd).getUri(), "video/mp4", null, mPosition, null,
                new ItemActionCallback() {
                    @Override
                    public void onResult(Bundle data, String sessionId,
                            MediaSessionStatus sessionStatus,
                            String itemId, MediaItemStatus itemStatus) {
                        if (DEBUG && !isSessionActive(sessionStatus)) {
                            Log.v(TAG, "play() is called, but session is not active.");
                        }
                        mItemId = itemId;
                        if (itemStatus != null) {
                            mDuration = itemStatus.getContentDuration();
                        }
                        // Do not update playback state here since this returns the buffering state.
                        // StatusCallback#onItemStatusChanged is called when the session reaches the
                        // play state.
                    }
                });
    }
}