From 2057a92a19e76cdbf903f238f8faaa12001138b7 Mon Sep 17 00:00:00 2001 From: Domingos Lopes Date: Mon, 21 Mar 2016 22:08:58 -0400 Subject: Add the casting feature to PlaybackService --- .../core/service/playback/LocalPSMP.java | 16 ++- .../core/service/playback/PlaybackService.java | 120 +++++++++++++++++++-- .../playback/PlaybackServiceMediaPlayer.java | 16 ++- .../core/service/playback/RemotePSMP.java | 12 ++- 4 files changed, 144 insertions(+), 20 deletions(-) (limited to 'core') diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 79b1537b0..373d587fb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -41,7 +41,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private final AudioManager audioManager; - private volatile PlayerStatus playerStatus; private volatile PlayerStatus statusBeforeSeeking; private volatile IPlayer mediaPlayer; private volatile Playable media; @@ -647,6 +646,15 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { releaseWifiLockIfNecessary(); } + /** + * Releases internally used resources. This method should only be called when the object is not used anymore. + * This method is executed on an internal executor service. + */ + @Override + public void shutdownAsync() { + executor.submit(this::shutdown); + } + @Override public void setVideoSurface(final SurfaceHolder surface) { executor.submit(() -> { @@ -780,7 +788,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override - public void endPlayback(final boolean wasSkipped) { + public void endPlayback(final boolean wasSkipped, boolean switchingPlayers) { executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); @@ -795,7 +803,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } audioManager.abandonAudioFocus(audioFocusChangeListener); - callback.endPlayback(isPlaying, wasSkipped); + callback.endPlayback(isPlaying, wasSkipped, switchingPlayers); playerLock.unlock(); }); @@ -873,7 +881,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { mp -> genericOnCompletion(); private void genericOnCompletion() { - endPlayback(false); + endPlayback(false, false); } private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 9595877bf..1c70c67ea 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -23,6 +23,7 @@ import android.preference.PreferenceManager; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; +import android.support.annotation.NonNull; import android.support.v7.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; @@ -35,6 +36,11 @@ import android.view.WindowManager; import android.widget.Toast; import com.bumptech.glide.Glide; +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; +import com.google.android.libraries.cast.companionlibrary.cast.VideoCastManager; +import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer; +import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl; import java.util.List; @@ -56,6 +62,7 @@ import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.IntList; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; /** @@ -123,6 +130,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar */ public static final int EXTRA_CODE_AUDIO = 1; public static final int EXTRA_CODE_VIDEO = 2; + public static final int EXTRA_CODE_CAST = 3; public static final int NOTIFICATION_TYPE_ERROR = 0; public static final int NOTIFICATION_TYPE_INFO = 1; @@ -171,6 +179,14 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar * Is true if the service was running, but paused due to headphone disconnect */ public static boolean transientPause = false; + /** + * Is true if a Cast Device is connected to the service. + */ + private static volatile boolean isCasting = false; + /** + * Stores the state of the cast playback just before it disconnects. + */ + private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection; private static final int NOTIFICATION_ID = 1; @@ -199,6 +215,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar return super.onUnbind(intent); } + //TODO review the general intent handling and how a casting activity can be introduced /** * Returns an intent which starts an audio- or videoplayer, depending on the * type of media that is being played. If the playbackservice is not @@ -231,6 +248,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar Log.d(TAG, "Service created."); isRunning = true; + VideoCastManager castMgr = VideoCastManager.getInstance(); + castMgr.addVideoCastConsumer(castConsumer); + isCasting = castMgr.isConnected(); + registerReceiver(headsetDisconnected, new IntentFilter( Intent.ACTION_HEADSET_PLUG)); registerReceiver(shutdownReceiver, new IntentFilter( @@ -296,6 +317,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver); unregisterReceiver(pauseResumeCurrentEpisodeReceiver); + VideoCastManager.getInstance().removeVideoCastConsumer(castConsumer); mediaPlayer.shutdown(); taskManager.shutdown(); } @@ -323,6 +345,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar if (keycode == -1 && playable == null) { Log.e(TAG, "PlaybackService was started with no arguments"); stopSelf(); + return Service.START_REDELIVER_INTENT; } if ((flags & Service.START_FLAG_REDELIVERY) != 0) { @@ -341,6 +364,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + //If the user asks to play External Media, the casting session, if on, should end. + if (playable instanceof ExternalMedia) { + VideoCastManager.getInstance().disconnect(); + } mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); } } @@ -397,7 +424,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar UserPreferences.shouldHardwareButtonSkip()) { // assume the skip command comes from a notification or the lockscreen // a >| skip button should actually skip - mediaPlayer.endPlayback(true); + mediaPlayer.endPlayback(true, false); } else { // assume skip command comes from a (bluetooth) media button // user actually wants to fast-forward @@ -619,13 +646,13 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar } @Override - public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) { - PlaybackService.this.endPlayback(playNextEpisode, wasSkipped); + public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { + PlaybackService.this.endPlayback(playNextEpisode, wasSkipped, switchingPlayers); return true; } }; - private void endPlayback(boolean playNextEpisode, boolean wasSkipped) { + private void endPlayback(boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { Log.d(TAG, "Playback ended"); final Playable playable = mediaPlayer.getPlayable(); @@ -724,9 +751,12 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar stream = !nextMedia.localFileAvailable(); mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + isCasting ? EXTRA_CODE_CAST : (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + if (!switchingPlayers) { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + } mediaPlayer.stop(); //stopSelf(); } @@ -1274,7 +1304,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - mediaPlayer.endPlayback(true); + mediaPlayer.endPlayback(true, false); } } }; @@ -1487,7 +1517,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar public void onSkipToNext() { Log.d(TAG, "onSkipToNext()"); if(UserPreferences.shouldHardwareButtonSkip()) { - mediaPlayer.endPlayback(true); + mediaPlayer.endPlayback(true, false); } else { seekDelta(UserPreferences.getFastFowardSecs() * 1000); } @@ -1514,4 +1544,80 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar return false; } }; + + private VideoCastConsumer castConsumer = new VideoCastConsumerImpl() { + @Override + public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { + Log.d(TAG, "A cast device application was connected"); + isCasting = true; + if (mediaPlayer != null) { + PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + if (info.playerStatus == PlayerStatus.PLAYING) { + // could be pause, but this way we make sure the new player will get the correct position, since pause runs asynchronously + saveCurrentPosition(false, 0); + } + } + switchMediaPlayer(new RemotePSMP(PlaybackService.this, mediaPlayerCallback), + (mediaPlayer != null) ? mediaPlayer.getPSMPInfo() : + new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null)); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, EXTRA_CODE_CAST); + } + + @Override + public void onDisconnectionReason(int reason) { + Log.d(TAG, "onDisconnectionReason() with code " + reason); + // This is our final chance to update the underlying stream position + // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer + // is disconnected and hence we update our local value of stream position + // to the latest position. + if (mediaPlayer != null) { + saveCurrentPosition(false, 0); + infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo(); + if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT && + infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) { + // If it's NOT based on user action, we shouldn't automatically resume local playback + infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED; + } + } + } + + @Override + public void onDisconnected() { + Log.d(TAG, "onDisconnected()"); + isCasting = false; + PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection; + infoBeforeCastDisconnection = null; + if (info == null && mediaPlayer != null) { + info = mediaPlayer.getPSMPInfo(); + } + if (info == null) { + info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null); + } + switchMediaPlayer(new LocalPSMP(PlaybackService.this, mediaPlayerCallback), + info); + if (info.playable != null) { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + info.playable.getMediaType() == MediaType.AUDIO ? EXTRA_CODE_AUDIO : EXTRA_CODE_VIDEO); + } else { + Log.d(TAG, "Cast session disconnected, but no current media"); + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + } + } + }; + + private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer, + @NonNull PlaybackServiceMediaPlayer.PSMPInfo info) { + if (mediaPlayer != null) { + mediaPlayer.endPlayback(true, true); + mediaPlayer.shutdownAsync(); + } + mediaPlayer = newPlayer; + Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName()); + if (info.playable != null) { + mediaPlayer.playMediaObject(info.playable, + !info.playable.localFileAvailable(), + info.playerStatus == PlayerStatus.PLAYING, + info.playerStatus.isAtLeast(PlayerStatus.PREPARING)); + } + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java index 736afbe3b..8ac808164 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -176,6 +176,12 @@ public abstract class PlaybackServiceMediaPlayer { */ public abstract void shutdown(); + /** + * Releases internally used resources. This method should only be called when the object is not used anymore. + * This method is executed on an internal executor service. + */ + public abstract void shutdownAsync(); + public abstract void setVideoSurface(SurfaceHolder surface); public abstract void resetVideoSurface(); @@ -218,10 +224,10 @@ public abstract class PlaybackServiceMediaPlayer { protected abstract void setPlayable(Playable playable); - public abstract void endPlayback(boolean wasSkipped); + public abstract void endPlayback(boolean wasSkipped, boolean switchingPlayers); /** - * Moves the LocalPSMP into STOPPED state. This call is only valid if the player is currently in + * Moves the PSMP into STOPPED state. This call is only valid if the player is currently in * INDETERMINATE state, for example after a call to endPlayback. * This method will only take care of changing the PlayerStatus of this object! Other tasks like * abandoning audio focus have to be done with other methods. @@ -238,7 +244,7 @@ public abstract class PlaybackServiceMediaPlayer { * @param newStatus The new PlayerStatus. This must not be null. * @param newMedia The new playable object of the PSMP object. This can be null. */ - protected synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { + protected synchronized final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { Log.d(TAG, "Setting player status to " + newStatus); this.playerStatus = newStatus; @@ -268,13 +274,13 @@ public abstract class PlaybackServiceMediaPlayer { boolean onMediaPlayerError(Object inObj, int what, int extra); - boolean endPlayback(boolean playNextEpisode, boolean wasSkipped); + boolean endPlayback(boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers); } /** * Holds information about a PSMP object. */ - public class PSMPInfo { + public static class PSMPInfo { public PlayerStatus playerStatus; public Playable playable; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java index 1dd19d3d7..d3b6c96cc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java @@ -124,14 +124,18 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { @Override public boolean isStreaming() { - //TODO - return false; + return true; } @Override public void shutdown() { //TODO - super.shutdown(); + } + + @Override + public void shutdownAsync() { + //TODO + this.shutdown(); } @Override @@ -162,7 +166,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { } @Override - public void endPlayback(boolean wasSkipped) { + public void endPlayback(boolean wasSkipped, boolean switchingPlayers) { //TODO } -- cgit v1.2.3