/*
* Copyright (C) 2014 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.example.android.uamp.playback;
import android.content.Context;
import android.net.Uri;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import com.example.android.uamp.model.MusicProvider;
import com.example.android.uamp.model.MusicProviderSource;
import com.example.android.uamp.utils.LogHelper;
import com.example.android.uamp.utils.MediaIDHelper;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.images.WebImage;
import org.json.JSONException;
import org.json.JSONObject;
import static android.support.v4.media.session.MediaSessionCompat.QueueItem;
/**
* An implementation of Playback that talks to Cast.
*/
public class CastPlayback implements Playback {
private static final String TAG = LogHelper.makeLogTag(CastPlayback.class);
private static final String MIME_TYPE_AUDIO_MPEG = "audio/mpeg";
private static final String ITEM_ID = "itemId";
private final MusicProvider mMusicProvider;
private final Context mAppContext;
private final RemoteMediaClient mRemoteMediaClient;
private final RemoteMediaClient.Listener mRemoteMediaClientListener;
/** The current PlaybackState*/
private int mState;
/** Callback for making completion/error calls on */
private Callback mCallback;
private volatile int mCurrentPosition;
private volatile String mCurrentMediaId;
public CastPlayback(MusicProvider musicProvider, Context context) {
mMusicProvider = musicProvider;
mAppContext = context.getApplicationContext();
CastSession castSession = CastContext.getSharedInstance(mAppContext).getSessionManager()
.getCurrentCastSession();
mRemoteMediaClient = castSession.getRemoteMediaClient();
mRemoteMediaClientListener = new CastMediaClientListener();
}
@Override
public void start() {
mRemoteMediaClient.addListener(mRemoteMediaClientListener);
}
@Override
public void stop(boolean notifyListeners) {
mRemoteMediaClient.removeListener(mRemoteMediaClientListener);
mState = PlaybackStateCompat.STATE_STOPPED;
if (notifyListeners && mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
}
@Override
public void setState(int state) {
this.mState = state;
}
@Override
public int getCurrentStreamPosition() {
if (!isConnected()) {
return mCurrentPosition;
}
return (int) mRemoteMediaClient.getApproximateStreamPosition();
}
@Override
public void setCurrentStreamPosition(int pos) {
this.mCurrentPosition = pos;
}
@Override
public void updateLastKnownStreamPosition() {
mCurrentPosition = getCurrentStreamPosition();
}
@Override
public void play(QueueItem item) {
try {
loadMedia(item.getDescription().getMediaId(), true);
mState = PlaybackStateCompat.STATE_BUFFERING;
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
} catch (JSONException e) {
LogHelper.e(TAG, "Exception loading media ", e, null);
if (mCallback != null) {
mCallback.onError(e.getMessage());
}
}
}
@Override
public void pause() {
try {
if (mRemoteMediaClient.hasMediaSession()) {
mRemoteMediaClient.pause();
mCurrentPosition = (int) mRemoteMediaClient.getApproximateStreamPosition();
} else {
loadMedia(mCurrentMediaId, false);
}
} catch (JSONException e) {
LogHelper.e(TAG, e, "Exception pausing cast playback");
if (mCallback != null) {
mCallback.onError(e.getMessage());
}
}
}
@Override
public void seekTo(int position) {
if (mCurrentMediaId == null) {
if (mCallback != null) {
mCallback.onError("seekTo cannot be calling in the absence of mediaId.");
}
return;
}
try {
if (mRemoteMediaClient.hasMediaSession()) {
mRemoteMediaClient.seek(position);
mCurrentPosition = position;
} else {
mCurrentPosition = position;
loadMedia(mCurrentMediaId, false);
}
} catch (JSONException e) {
LogHelper.e(TAG, e, "Exception pausing cast playback");
if (mCallback != null) {
mCallback.onError(e.getMessage());
}
}
}
@Override
public void setCurrentMediaId(String mediaId) {
this.mCurrentMediaId = mediaId;
}
@Override
public String getCurrentMediaId() {
return mCurrentMediaId;
}
@Override
public void setCallback(Callback callback) {
this.mCallback = callback;
}
@Override
public boolean isConnected() {
CastSession castSession = CastContext.getSharedInstance(mAppContext).getSessionManager()
.getCurrentCastSession();
return (castSession != null && castSession.isConnected());
}
@Override
public boolean isPlaying() {
return isConnected() && mRemoteMediaClient.isPlaying();
}
@Override
public int getState() {
return mState;
}
private void loadMedia(String mediaId, boolean autoPlay) throws JSONException {
String musicId = MediaIDHelper.extractMusicIDFromMediaID(mediaId);
MediaMetadataCompat track = mMusicProvider.getMusic(musicId);
if (track == null) {
throw new IllegalArgumentException("Invalid mediaId " + mediaId);
}
if (!TextUtils.equals(mediaId, mCurrentMediaId)) {
mCurrentMediaId = mediaId;
mCurrentPosition = 0;
}
JSONObject customData = new JSONObject();
customData.put(ITEM_ID, mediaId);
MediaInfo media = toCastMediaMetadata(track, customData);
mRemoteMediaClient.load(media, autoPlay, mCurrentPosition, customData);
}
/**
* Helper method to convert a {@link android.media.MediaMetadata} to a
* {@link com.google.android.gms.cast.MediaInfo} used for sending media to the receiver app.
*
* @param track {@link com.google.android.gms.cast.MediaMetadata}
* @param customData custom data specifies the local mediaId used by the player.
* @return mediaInfo {@link com.google.android.gms.cast.MediaInfo}
*/
private static MediaInfo toCastMediaMetadata(MediaMetadataCompat track,
JSONObject customData) {
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK);
mediaMetadata.putString(MediaMetadata.KEY_TITLE,
track.getDescription().getTitle() == null ? "" :
track.getDescription().getTitle().toString());
mediaMetadata.putString(MediaMetadata.KEY_SUBTITLE,
track.getDescription().getSubtitle() == null ? "" :
track.getDescription().getSubtitle().toString());
mediaMetadata.putString(MediaMetadata.KEY_ALBUM_ARTIST,
track.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST));
mediaMetadata.putString(MediaMetadata.KEY_ALBUM_TITLE,
track.getString(MediaMetadataCompat.METADATA_KEY_ALBUM));
WebImage image = new WebImage(
new Uri.Builder().encodedPath(
track.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI))
.build());
// First image is used by the receiver for showing the audio album art.
mediaMetadata.addImage(image);
// Second image is used by Cast Companion Library on the full screen activity that is shown
// when the cast dialog is clicked.
mediaMetadata.addImage(image);
//noinspection ResourceType
return new MediaInfo.Builder(track.getString(MusicProviderSource.CUSTOM_METADATA_TRACK_SOURCE))
.setContentType(MIME_TYPE_AUDIO_MPEG)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mediaMetadata)
.setCustomData(customData)
.build();
}
private void setMetadataFromRemote() {
// Sync: We get the customData from the remote media information and update the local
// metadata if it happens to be different from the one we are currently using.
// This can happen when the app was either restarted/disconnected + connected, or if the
// app joins an existing session while the Chromecast was playing a queue.
try {
MediaInfo mediaInfo = mRemoteMediaClient.getMediaInfo();
if (mediaInfo == null) {
return;
}
JSONObject customData = mediaInfo.getCustomData();
if (customData != null && customData.has(ITEM_ID)) {
String remoteMediaId = customData.getString(ITEM_ID);
if (!TextUtils.equals(mCurrentMediaId, remoteMediaId)) {
mCurrentMediaId = remoteMediaId;
if (mCallback != null) {
mCallback.setCurrentMediaId(remoteMediaId);
}
updateLastKnownStreamPosition();
}
}
} catch (JSONException e) {
LogHelper.e(TAG, e, "Exception processing update metadata");
}
}
private void updatePlaybackState() {
int status = mRemoteMediaClient.getPlayerState();
int idleReason = mRemoteMediaClient.getIdleReason();
LogHelper.d(TAG, "onRemoteMediaPlayerStatusUpdated ", status);
// Convert the remote playback states to media playback states.
switch (status) {
case MediaStatus.PLAYER_STATE_IDLE:
if (idleReason == MediaStatus.IDLE_REASON_FINISHED) {
if (mCallback != null) {
mCallback.onCompletion();
}
}
break;
case MediaStatus.PLAYER_STATE_BUFFERING:
mState = PlaybackStateCompat.STATE_BUFFERING;
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
break;
case MediaStatus.PLAYER_STATE_PLAYING:
mState = PlaybackStateCompat.STATE_PLAYING;
setMetadataFromRemote();
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
break;
case MediaStatus.PLAYER_STATE_PAUSED:
mState = PlaybackStateCompat.STATE_PAUSED;
setMetadataFromRemote();
if (mCallback != null) {
mCallback.onPlaybackStatusChanged(mState);
}
break;
default: // case unknown
LogHelper.d(TAG, "State default : ", status);
break;
}
}
private class CastMediaClientListener implements RemoteMediaClient.Listener {
@Override
public void onMetadataUpdated() {
LogHelper.d(TAG, "RemoteMediaClient.onMetadataUpdated");
setMetadataFromRemote();
}
@Override
public void onStatusUpdated() {
LogHelper.d(TAG, "RemoteMediaClient.onStatusUpdated");
updatePlaybackState();
}
@Override
public void onSendingRemoteMediaRequest() {
}
@Override
public void onAdBreakStatusUpdated() {
}
@Override
public void onQueueStatusUpdated() {
}
@Override
public void onPreloadStatusUpdated() {
}
}
}