diff options
Diffstat (limited to 'core')
10 files changed, 556 insertions, 338 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 64d4d14fd..d4414227c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.feed; +import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; @@ -14,12 +15,16 @@ import java.util.Date; import java.util.List; import java.util.concurrent.Callable; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; import de.danoeh.antennapod.core.util.playback.Playable; public class FeedMedia extends FeedFile implements Playable { @@ -48,6 +53,8 @@ public class FeedMedia extends FeedFile implements Playable { private String mime_type; @Nullable private volatile FeedItem item; private Date playbackCompletionDate; + private int startPosition = -1; + private int playedDurationWhenStarted; // if null: unknown, will be checked private Boolean hasEmbeddedPicture; @@ -73,6 +80,7 @@ public class FeedMedia extends FeedFile implements Playable { this.duration = duration; this.position = position; this.played_duration = played_duration; + this.playedDurationWhenStarted = played_duration; this.size = size; this.mime_type = mime_type; this.playbackCompletionDate = playbackCompletionDate == null @@ -472,15 +480,59 @@ public class FeedMedia extends FeedFile implements Playable { } setPosition(newPosition); setLastPlayedTime(timeStamp); + if(startPosition>=0 && position > startPosition) { + setPlayedDuration(playedDurationWhenStarted + position - startPosition); + } DBWriter.setFeedMediaPlaybackInformation(this); } @Override public void onPlaybackStart() { + startPosition = (position > 0) ? position : 0; + playedDurationWhenStarted = played_duration; } + @Override - public void onPlaybackCompleted() { + public void onPlaybackPause(Context context) { + if (position > startPosition) { + played_duration = playedDurationWhenStarted + position - startPosition; + playedDurationWhenStarted = played_duration; + } + postPlaybackTasks(context, false); + startPosition = position; + } + @Override + public void onPlaybackCompleted(Context context) { + postPlaybackTasks(context, true); + startPosition = -1; + } + + private void postPlaybackTasks(Context context, boolean completed) { + if (item != null) { + // gpodder play action + if (startPosition >= 0 && (completed || startPosition < position) && + GpodnetPreferences.loggedIn()) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.PLAY) + .currentDeviceId() + .currentTimestamp() + .started(startPosition / 1000) + .position((completed ? duration : position) / 1000) + .total(duration / 1000) + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } + // Auto flattr + float autoFlattrThreshold = UserPreferences.getAutoFlattrPlayedDurationThreshold(); + if (FlattrUtils.hasToken() && + UserPreferences.isAutoFlattr() && + item.getPaymentLink() != null && + item.getFlattrStatus().getUnflattred() && + (completed && autoFlattrThreshold <= 1.0f || + played_duration >= autoFlattrThreshold * duration)) { + DBTasks.flattrItemIfLoggedIn(context, item); + } + } } @Override 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 8bacac1ef..0871758d0 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 @@ -13,6 +13,7 @@ import org.antennapod.audio.MediaPlayer; import java.io.IOException; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -140,10 +141,13 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } // set temporarily to pause in order to update list with current position if (playerStatus == PlayerStatus.PLAYING) { - setPlayerStatus(PlayerStatus.PAUSED, media); + callback.onPlaybackPause(media, getPosition()); } - smartMarkAsPlayed(media); + if (!media.getIdentifier().equals(playable.getIdentifier())) { + final Playable oldMedia = media; + executor.submit(() -> callback.onPostPlayback(oldMedia, false, true)); + } setPlayerStatus(PlayerStatus.INDETERMINATE, null); } @@ -199,6 +203,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(TAG, "Audiofocus successfully requested"); + Log.d(TAG, "Resuming/Starting playback"); acquireWifiLockIfNecessary(); float speed = 1.0f; try { @@ -220,8 +226,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.PLAYING, media); pausedBecauseOfTransientAudiofocusLoss = false; - media.onPlaybackStart(); - } else { Log.e(TAG, "Failed to request audio focus"); } @@ -249,7 +253,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Pausing playback."); mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media); + setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); if (abandonFocus) { audioManager.abandonAudioFocus(audioFocusChangeListener); @@ -311,11 +315,12 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); } + // TODO this call has no effect! if (media.getPosition() > 0) { seekToSync(media.getPosition()); } - if (media.getDuration() == 0) { + if (media.getDuration() <= 0) { Log.d(TAG, "Setting duration of media"); media.setDuration(mediaPlayer.getDuration()); } @@ -367,10 +372,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - if (!stream) { - statusBeforeSeeking = playerStatus; - setPlayerStatus(PlayerStatus.SEEKING, media); - } if(seekLatch != null && seekLatch.getCount() > 0) { try { seekLatch.await(3, TimeUnit.SECONDS); @@ -379,6 +380,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } } seekLatch = new CountDownLatch(1); + statusBeforeSeeking = playerStatus; + setPlayerStatus(PlayerStatus.SEEKING, media, getPosition()); mediaPlayer.seekTo(t); try { seekLatch.await(3, TimeUnit.SECONDS); @@ -752,8 +755,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override - public void endPlayback(final boolean wasSkipped, boolean switchingPlayers) { - executor.submit(() -> { + protected Future<?> endPlayback(final boolean wasSkipped, final boolean shouldContinue, final boolean toStoppedState) { + return executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); @@ -762,13 +765,58 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (playerStatus != PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.INDETERMINATE, media); } + // we're relying on the position stored in the Playable object for post-playback processing + if (media != null) { + int position = getPosition(); + if (position >= 0) { + media.setPosition(position); + } + } + if (mediaPlayer != null) { mediaPlayer.reset(); - } audioManager.abandonAudioFocus(audioFocusChangeListener); - callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers); + final Playable currentMedia = media; + Playable nextMedia = null; + + if (shouldContinue) { + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + nextMedia = callback.getNextInQueue(currentMedia); + + boolean playNextEpisode = isPlaying && + nextMedia != null && + UserPreferences.isFollowQueue(); + + if (playNextEpisode) { + Log.d(TAG, "Playback of next episode will start immediately."); + } else if (nextMedia == null){ + Log.d(TAG, "No more episodes available to play"); + } else { + Log.d(TAG, "Loading next episode, but not playing automatically."); + } + + if (nextMedia != null) { + callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode); + // setting media to null signals to playMediaObject() that we're taking care of post-playback processing + media = null; + playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), playNextEpisode, playNextEpisode); + } + } + if (shouldContinue || toStoppedState) { + if (nextMedia == null) { + callback.onPlaybackEnded(null, true); + stop(); + } + final boolean hasNext = nextMedia != null; + + executor.submit(() -> callback.onPostPlayback(currentMedia, !wasSkipped, hasNext)); + } else if (isPlaying) { + callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); + } playerLock.unlock(); }); } @@ -779,8 +827,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * 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. */ - @Override - public void stop() { + private void stop() { executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); @@ -833,7 +880,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { mp -> genericOnCompletion(); private void genericOnCompletion() { - endPlayback(false, false); + endPlayback(false, true, true); } private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = @@ -889,8 +936,11 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { seekLatch.countDown(); } playerLock.lock(); + if (playerStatus == PlayerStatus.PLAYING) { + callback.onPlaybackStart(media, getPosition()); + } if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media); + setPlayerStatus(statusBeforeSeeking, media, getPosition()); } playerLock.unlock(); }); 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 e3557f5f8..04b5b676d 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 @@ -52,9 +52,6 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; @@ -63,7 +60,6 @@ import de.danoeh.antennapod.core.storage.DBTasks; 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; @@ -206,8 +202,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private MediaSessionCompat mediaSession; - private int startPosition; - private static volatile MediaType currentMediaType = MediaType.UNKNOWN; private final IBinder mBinder = new LocalBinder(); @@ -473,7 +467,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { UserPreferences.shouldHardwareButtonSkip()) { // assume the skip command comes from a notification or the lockscreen // a >| skip button should actually skip - mediaPlayer.endPlayback(true, false); + mediaPlayer.skip(); } else { // assume skip command comes from a (bluetooth) media button // user actually wants to fast-forward @@ -530,7 +524,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { @Override public void positionSaverTick() { - saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); + saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); } @Override @@ -582,9 +576,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case PAUSED: - taskManager.cancelPositionSaver(); - saveCurrentPosition(false, 0); - taskManager.cancelWidgetUpdater(); if ((UserPreferences.isPersistNotify() || isCasting) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // do not remove notification on pause based on user pref and whether android version supports expanded notifications @@ -595,22 +586,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { stopForeground(true); } writePlayerStatusPlaybackPreferences(); - - final Playable playable = newInfo.playable; - - // Gpodder: send play action - if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getCurrentPosition() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } break; case STOPPED: @@ -619,15 +594,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case PLAYING: - Log.d(TAG, "Audiofocus successfully requested"); - Log.d(TAG, "Resuming/Starting playback"); - - taskManager.startPositionSaver(); - taskManager.startWidgetUpdater(); writePlayerStatusPlaybackPreferences(); setupNotification(newInfo); started = true; - startPosition = mediaPlayer.getPosition(); break; case ERROR: @@ -700,121 +669,168 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - PlaybackService.this.endPlayback(media, playNextEpisode, wasSkipped, switchingPlayers); - return true; + public void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext) { + PlaybackService.this.onPostPlayback(media, ended, playingNext); + } + + @Override + public void onPlaybackStart(@NonNull Playable playable, int position) { + taskManager.startWidgetUpdater(); + if (position != PlaybackServiceMediaPlayer.INVALID_TIME) { + playable.setPosition(position); + } + playable.onPlaybackStart(); + taskManager.startPositionSaver(); + } + + @Override + public void onPlaybackPause(Playable playable, int position) { + taskManager.cancelPositionSaver(); + saveCurrentPosition(position == PlaybackServiceMediaPlayer.INVALID_TIME || playable == null, + playable, position); + taskManager.cancelWidgetUpdater(); + if (playable != null) { + playable.onPlaybackPause(getApplicationContext()); + } + } + + @Override + public Playable getNextInQueue(Playable currentMedia) { + return PlaybackService.this.getNextInQueue(currentMedia); + } + + @Override + public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying); } }; - private void endPlayback(final Playable playable, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - Log.d(TAG, "Playback ended" + (switchingPlayers ? " from switching players": "")); + private Playable getNextInQueue(final Playable currentMedia) { + if (!(currentMedia instanceof FeedMedia)) { + Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding"); + return null; + } + if (!ClientConfig.playbackServiceCallbacks.useQueue()) { + Log.d(TAG, "getNextInQueue(), but queue not in use by this app"); + return null; + } + Log.d(TAG, "getNextInQueue()"); + FeedMedia media = (FeedMedia) currentMedia; + try { + media.loadMetadata(); + } catch (Playable.PlayableException e) { + Log.e(TAG, "Unable to load metadata to get next in queue", e); + return null; + } + FeedItem item = media.getItem(); + if (item == null) { + Log.w(TAG, "getNextInQueue() with FeedMedia object whose FeedItem is null"); + return null; + } + FeedItem nextItem; + try { + final List<FeedItem> queue = taskManager.getQueue(); + nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); + } catch (InterruptedException e) { + Log.e(TAG, "Error handling the queue in order to retrieve the next item", e); + return null; + } + return (nextItem != null)? nextItem.getMedia() : null; + + } + /** + * Set of instructions to be performed when playback ends. + */ + private void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { + Log.d(TAG, "Playback ended"); + if (stopPlaying) { + taskManager.cancelPositionSaver(); + writePlaybackPreferencesNoMediaPlaying(); + if (!isCasting) { + stopForeground(true); + } + stopWidgetUpdater(); + } + if (mediaType == null) { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + isCasting ? EXTRA_CODE_CAST : + (mediaType == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); + } + } + + /** + * This method processes the media object after its playback ended, either because it completed + * or because a different media object was selected for playback. + * + * Even though these tasks aren't supposed to be resource intensive, a good practice is to + * usually call this method on a background thread. + * + * @param playable the media object that was playing. It is assumed that its position property + * was updated before this method was called. + * @param ended if true, it signals that {@param playable} was played until its end. + * In such case, the position property of the media becomes irrelevant for most of + * the tasks (although it's still a good practice to keep it accurate). + * @param playingNext if true, it means another media object is being loaded in place of this one. + * Instances when we'd set it to false would be when we're not following the + * queue or when the queue has ended. + */ + private void onPostPlayback(final Playable playable, boolean ended, boolean playingNext) { if (playable == null) { - Log.e(TAG, "Cannot end playback: media was null"); + Log.e(TAG, "Cannot do post-playback processing: media was null"); return; } + Log.d(TAG, "onPostPlayback(): media=" + playable.getEpisodeTitle()); - taskManager.cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; + if (!(playable instanceof FeedMedia)) { + Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia"); + if (ended) { + playable.onPlaybackCompleted(getApplicationContext()); + } else { + playable.onPlaybackPause(getApplicationContext()); + } + return; + } + FeedMedia media = (FeedMedia) playable; + FeedItem item = media.getItem(); + boolean smartMarkAsPlayed = playingNext && media.hasAlmostEnded(); + if (!ended && smartMarkAsPlayed) { + Log.d(TAG, "smart mark as played"); + } - if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); + if (ended || smartMarkAsPlayed) { + media.onPlaybackCompleted(getApplicationContext()); + } else { + media.onPlaybackPause(getApplicationContext()); + } - if (!switchingPlayers) { + if (item != null) { + if (ended || smartMarkAsPlayed || + !UserPreferences.shouldSkipKeepEpisode()) { + // only mark the item as played if we're not keeping it anyways + DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended); try { final List<FeedItem> queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); - nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); + if (QueueAccess.ItemListAccess(queue).contains(item.getId())) { + // don't know if it actually matters to not autodownload when smart mark as played is triggered + DBWriter.removeQueueItem(PlaybackService.this, item, ended); + } } catch (InterruptedException e) { e.printStackTrace(); // isInQueue remains false } - - boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode(); - - if (!shouldKeep) { - // only mark the item as played if we're not keeping it anyways - DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); - - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item, true); - } - - // Delete episode if enabled - if (item.getFeed().getPreferences().getCurrentAutoDelete()) { - DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); - Log.d(TAG, "Episode Deleted"); - } + // Delete episode if enabled + if (item.getFeed().getPreferences().getCurrentAutoDelete()) { + DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); + Log.d(TAG, "Episode Deleted"); } } - - - DBWriter.addItemToPlaybackHistory(media); - - // auto-flattr if enabled - if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { - DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); - } - - // gpodder play action - if(GpodnetPreferences.loggedIn()) { - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getDuration() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } } - if (!switchingPlayers) { - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - Playable nextMedia = null; - boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() && - isInQueue && - nextItem != null; - - playNextEpisode = playNextEpisode && - loadNextItem && - UserPreferences.isFollowQueue(); - - if (loadNextItem) { - Log.d(TAG, "Loading next item in queue"); - nextMedia = nextItem.getMedia(); - } - final boolean prepareImmediately; - final boolean startWhenPrepared; - final boolean stream; - - if (playNextEpisode) { - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - Log.d(TAG, "No more episodes available to play"); - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - writePlaybackPreferencesNoMediaPlaying(); - if (nextMedia != null) { - 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); - mediaPlayer.stop(); - //stopSelf(); - } + if (ended || playingNext) { + DBWriter.addItemToPlaybackHistory(media); } } @@ -1218,28 +1234,23 @@ public class PlaybackService extends MediaBrowserServiceCompat { /** * Persists the current position and last played time of the media file. * - * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects - * @param deltaPlayedDuration value by which played_duration should be increased. + * @param fromMediaPlayer if true, the information is gathered from the current Media Player + * and {@param playable} and {@param position} become irrelevant. + * @param playable the playable for which the current position should be saved, unless + * {@param fromMediaPlayer} is true. + * @param position the position that should be saved, unless {@param fromMediaPlayer} is true. */ - private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - int position = getCurrentPosition(); - int duration = getDuration(); - float playbackSpeed = getCurrentPlaybackSpeed(); - final Playable playable = mediaPlayer.getPlayable(); + private synchronized void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { + int duration; + if (fromMediaPlayer) { + position = getCurrentPosition(); + duration = getDuration(); + playable = mediaPlayer.getPlayable(); + } else { + duration = playable.getDuration(); + } if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { Log.d(TAG, "Saving current position to " + position); - if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); - // Auto flattr - if (isAutoFlattrable(media) && - (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration()) - + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); - DBTasks.flattrItemIfLoggedIn(this, item); - } - } playable.saveCurrentPosition( PreferenceManager.getDefaultSharedPreferences(getApplicationContext()), position, @@ -1407,7 +1418,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { 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, false); + mediaPlayer.skip(); } } }; @@ -1500,26 +1511,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void seekTo(final int t) { - if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING - && GpodnetPreferences.loggedIn()) { - final Playable playable = mediaPlayer.getPlayable(); - if (playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getCurrentPosition() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } - } mediaPlayer.seekTo(t); - if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) { - startPosition = t; - } } @@ -1528,10 +1520,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { } /** - * @see LocalPSMP#seekToChapter(de.danoeh.antennapod.core.feed.Chapter) + * Seek to the start of the specified chapter. */ public void seekToChapter(Chapter c) { - mediaPlayer.seekToChapter(c); + seekTo((int) c.getStart()); } /** @@ -1558,15 +1550,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { return mediaPlayer.getVideoSize(); } - private boolean isAutoFlattrable(FeedMedia media) { - if (media != null) { - FeedItem item = media.getItem(); - return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); - } else { - return false; - } - } - private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { private static final String TAG = "MediaSessionCompat"; @@ -1602,19 +1585,14 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void onPause() { Log.d(TAG, "onPause()"); if (getStatus() == PlayerStatus.PLAYING) { - pause(false, true); - } - if (UserPreferences.isPersistNotify()) { - pause(false, true); - } else { - pause(true, true); + pause(!UserPreferences.isPersistNotify(), true); } } @Override public void onStop() { Log.d(TAG, "onStop()"); - mediaPlayer.stop(); + mediaPlayer.stopPlayback(true); } @Override @@ -1639,7 +1617,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void onSkipToNext() { Log.d(TAG, "onSkipToNext()"); if(UserPreferences.shouldHardwareButtonSkip()) { - mediaPlayer.endPlayback(true, false); + mediaPlayer.skip(); } else { seekDelta(UserPreferences.getFastFowardSecs() * 1000); } @@ -1682,7 +1660,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { PlaybackServiceMediaPlayer getMediaPlayer(); void setIsCasting(boolean isCasting); void sendNotificationBroadcast(int type, int code); - void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration); + void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info); MediaSessionCompat getMediaSession(); Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); @@ -1716,8 +1694,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - PlaybackService.this.saveCurrentPosition(updatePlayedDuration, deltaPlayedDuration); + public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { + PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position); } @Override 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 e05733135..aec059ca0 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 @@ -8,11 +8,9 @@ import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; +import java.util.concurrent.Future; + import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.playback.Playable; @@ -130,13 +128,6 @@ public abstract class PlaybackServiceMediaPlayer { public abstract void seekDelta(int d); /** - * Seek to the start of the specified chapter. - */ - public void seekToChapter(@NonNull Chapter c) { - seekTo((int) c.getStart()); - } - - /** * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. */ public abstract int getDuration(); @@ -236,15 +227,44 @@ public abstract class PlaybackServiceMediaPlayer { protected abstract void setPlayable(Playable playable); - public abstract void endPlayback(boolean wasSkipped, boolean switchingPlayers); + public void skip() { + endPlayback(true, true, true); + } /** - * 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. + * Ends playback of current media (if any) and moves into INDETERMINATE state, unless + * {@param toStoppedState} is set to true, in which case it moves into STOPPED state. + * + * @see #endPlayback(boolean, boolean, boolean) */ - public abstract void stop(); + public Future<?> stopPlayback(boolean toStoppedState) { + return endPlayback(true, false, toStoppedState); + } + + /** + * Internal method that handles end of playback. + * + * Currently, it has 4 use cases: + * <ul> + * <li>Media playback has completed: call with (false, true, true)</li> + * <li>User asks to skip to next episode: call with (true, true, true)</li> + * <li>Stopping the media player: call with (true, false, true)</li> + * <li>We want to change the media player implementation: call with (true, false, false)</li> + * </ul> + * + * @param wasSkipped If true, we assume the current media's playback has ended, for + * purposes of post playback processing. + * @param shouldContinue If true, the media player should try to load, and possibly play, + * the next item, based on the user preferences and whether such item + * exists. + * @param toStoppedState If true, the playback state gets set to STOPPED if the media player + * is not loading/playing after this call, and the UI will reflect that. + * Only relevant if {@param shouldContinue} is set to false, otherwise + * this method's behavior defaults as if this parameter was true. + * + * @return a Future, just for the purpose of tracking its execution. + */ + protected abstract Future<?> endPlayback(boolean wasSkipped, boolean shouldContinue, boolean toStoppedState); /** * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise. @@ -274,41 +294,39 @@ public abstract class PlaybackServiceMediaPlayer { * <p/> * This method will notify the callback about the change of the player status (even if the new status is the same * as the old one). + * <p/> + * It will also call {@link PSMPCallback#onPlaybackPause(Playable, int)} or {@link PSMPCallback#onPlaybackStart(Playable, int)} + * depending on the status change. * * @param newStatus The new PlayerStatus. This must not be null. * @param newMedia The new playable object of the PSMP object. This can be null. + * @param position The position to be set to the current Playable object in case playback started or paused. + * Will be ignored if given the value of {@link #INVALID_TIME}. */ - protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { + protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) { Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus); + PlayerStatus oldStatus = playerStatus; + this.playerStatus = newStatus; setPlayable(newMedia); - if (playerStatus != null) { - Log.d(TAG, "playerStatus: " + playerStatus.toString()); + if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) { + if (oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) { + callback.onPlaybackPause(newMedia, position); + } else if (oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) { + callback.onPlaybackStart(newMedia, position); + } } callback.statusChanged(new PSMPInfo(playerStatus, getPlayable())); } - protected void smartMarkAsPlayed(Playable media) { - if(media != null && media instanceof FeedMedia) { - FeedMedia oldMedia = (FeedMedia) media; - if(oldMedia.hasAlmostEnded()) { - Log.d(TAG, "smart mark as read"); - FeedItem item = oldMedia.getItem(); - if (item == null) { - return; - } - DBWriter.markItemPlayed(item, FeedItem.PLAYED, false); - DBWriter.removeQueueItem(context, item, false); - DBWriter.addItemToPlaybackHistory(oldMedia); - if (item.getFeed().getPreferences().getCurrentAutoDelete()) { - Log.d(TAG, "Delete " + oldMedia.toString()); - DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId()); - } - } - } + /** + * @see #setPlayerStatus(PlayerStatus, Playable, int) + */ + protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { + setPlayerStatus(newStatus, newMedia, INVALID_TIME); } public interface PSMPCallback { @@ -328,7 +346,15 @@ public abstract class PlaybackServiceMediaPlayer { boolean onMediaPlayerError(Object inObj, int what, int extra); - boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers); + void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext); + + void onPlaybackStart(@NonNull Playable playable, int position); + + void onPlaybackPause(Playable playable, int position); + + Playable getNextInQueue(Playable currentMedia); + + void onPlaybackEnded(MediaType mediaType, boolean stopPlaying); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java index a37f98469..c4acdb65e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.util.playback; +import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.media.MediaMetadataRetriever; @@ -205,7 +206,12 @@ public class ExternalMedia implements Playable { } @Override - public void onPlaybackCompleted() { + public void onPlaybackPause(Context context) { + + } + + @Override + public void onPlaybackCompleted(Context context) { } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java index 6459d86ed..279c56338 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -138,14 +138,27 @@ public interface Playable extends Parcelable, void setLastPlayedTime(long lastPlayedTimestamp); /** - * Is called by the PlaybackService when playback starts. + * This method should be called every time playback starts on this object. + * <p/> + * Position held by this Playable should be set accurately before a call to this method is made. */ void onPlaybackStart(); /** - * Is called by the PlaybackService when playback is completed. + * This method should be called every time playback pauses or stops on this object, + * including just before a seeking operation is performed, after which a call to + * {@link #onPlaybackStart()} should be made. If playback completes, calling this method is not + * necessary, as long as a call to {@link #onPlaybackCompleted(Context)} is made. + * <p/> + * Position held by this Playable should be set accurately before a call to this method is made. */ - void onPlaybackCompleted(); + void onPlaybackPause(Context context); + + /** + * This method should be called when playback completes for this object. + * @param context + */ + void onPlaybackCompleted(Context context); /** * Returns an integer that must be unique among all Playable classes. The diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java index 0674990f1..27fb7344d 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java @@ -142,7 +142,6 @@ public class CastManager extends BaseCastManager implements OnFailedListener { if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance() .isGooglePlayServicesAvailable(context)) { Log.e(TAG, "Couldn't find the appropriate version of Google Play Services"); - //TODO check whether creating an instance without google play services installed actually gives an exception } INSTANCE = new CastManager(context, castConfiguration); } diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java b/core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java index e2d8f8ad5..a3ac87062 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/RemoteMedia.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.cast; +import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Parcel; @@ -255,7 +256,12 @@ public class RemoteMedia implements Playable { } @Override - public void onPlaybackCompleted() { + public void onPlaybackPause(Context context) { + // no-op + } + + @Override + public void onPlaybackCompleted(Context context) { // no-op } diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java index 93431d466..c7428947b 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java @@ -15,6 +15,8 @@ import android.widget.Toast; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; +import java.util.concurrent.ExecutionException; + import de.danoeh.antennapod.core.cast.CastConsumer; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.DefaultCastConsumer; @@ -108,7 +110,7 @@ public class PlaybackServiceFlavorHelper { // to the latest position. PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer(); if (mediaPlayer != null) { - callback.saveCurrentPosition(false, 0); + callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo(); if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT && infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) { @@ -160,7 +162,7 @@ public class PlaybackServiceFlavorHelper { // could be pause, but this way we make sure the new player will get the correct position, // since pause runs asynchronously and we could be directing the new player to play even before // the old player gives us back the position. - callback.saveCurrentPosition(false, 0); + callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); } } if (info == null) { @@ -182,7 +184,11 @@ public class PlaybackServiceFlavorHelper { boolean wasLaunched) { PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer(); if (mediaPlayer != null) { - mediaPlayer.endPlayback(true, true); + try { + mediaPlayer.stopPlayback(false).get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "There was a problem stopping playback while switching media players", e); + } mediaPlayer.shutdownQuietly(); } mediaPlayer = newPlayer; diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java index 4262b8a70..ea95ea894 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java @@ -15,6 +15,8 @@ import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastEx import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; import de.danoeh.antennapod.core.R; @@ -25,6 +27,7 @@ import de.danoeh.antennapod.core.cast.DefaultCastConsumer; import de.danoeh.antennapod.core.cast.RemoteMedia; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; import de.danoeh.antennapod.core.util.playback.Playable; @@ -42,8 +45,9 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { private final CastManager castMgr; private volatile Playable media; - private volatile MediaInfo remoteMedia; private volatile MediaType mediaType; + private volatile MediaInfo remoteMedia; + private volatile int remoteState; private final AtomicBoolean isBuffering; @@ -57,31 +61,28 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { mediaType = null; startWhenPrepared = new AtomicBoolean(false); isBuffering = new AtomicBoolean(false); + remoteState = MediaStatus.PLAYER_STATE_UNKNOWN; try { if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) { - // updates the state, but does not start playing new media if it was going to - onRemoteMediaPlayerStatusUpdated( - ((p, playNextEpisode, wasSkipped, switchingPlayers) -> - this.callback.endPlayback(p, false, wasSkipped, switchingPlayers))); + onRemoteMediaPlayerStatusUpdated(); } } catch (TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "Unable to do initial check for loaded media", e); } castMgr.addCastConsumer(castConsumer); - //TODO } private CastConsumer castConsumer = new DefaultCastConsumer() { @Override public void onRemoteMediaPlayerMetadataUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback); + RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); } @Override public void onRemoteMediaPlayerStatusUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback); + RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); } @Override @@ -121,8 +122,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()"); } if (playbackEnded) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - callback.endPlayback(media, true, false, false); + // This is an unconventional thing to occur... + endPlayback(true, true, true); } } @@ -166,85 +167,123 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { return null; } - private void onRemoteMediaPlayerStatusUpdated(@NonNull EndPlaybackCall endPlaybackCall) { + private void onRemoteMediaPlayerStatusUpdated() { MediaStatus status = castMgr.getMediaStatus(); if (status == null) { Log.d(TAG, "Received null MediaStatus"); - //setBuffering(false); - //setPlayerStatus(PlayerStatus.INDETERMINATE, null); return; } else { Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState()); } - Playable currentMedia = localVersion(status.getMediaInfo()); - boolean updateUI = currentMedia != media; - if (currentMedia != null) { - long position = status.getStreamPosition(); - if (position > 0 && currentMedia.getPosition() == 0) { - currentMedia.setPosition((int) position); - } - } int state = status.getPlayerState(); + int oldState = remoteState; + remoteMedia = status.getMediaInfo(); + boolean mediaChanged = !CastUtils.matches(remoteMedia, media); + boolean stateChanged = state != oldState; + if (!mediaChanged && !stateChanged) { + Log.d(TAG, "Both media and state haven't changed, so nothing to do"); + return; + } + Playable currentMedia = mediaChanged ? localVersion(remoteMedia) : media; + Playable oldMedia = media; + int position = (int) status.getStreamPosition(); + // check for incompatible states + if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED) + && currentMedia == null) { + Log.w(TAG, "RemoteMediaPlayer returned playing or pausing state, but with no media"); + state = MediaStatus.PLAYER_STATE_UNKNOWN; + stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN; + } + + if (stateChanged) { + remoteState = state; + } + + if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && + state != MediaStatus.PLAYER_STATE_IDLE) { + callback.onPlaybackPause(null, INVALID_TIME); + // We don't want setPlayerStatus to handle the onPlaybackPause callback + setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); + } + setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING); + switch (state) { case MediaStatus.PLAYER_STATE_PLAYING: - setPlayerStatus(PlayerStatus.PLAYING, currentMedia); + if (!stateChanged) { + //These steps are necessary because they won't be performed by setPlayerStatus() + if (position >= 0) { + currentMedia.setPosition(position); + } + currentMedia.onPlaybackStart(); + } + setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position); break; case MediaStatus.PLAYER_STATE_PAUSED: - setPlayerStatus(PlayerStatus.PAUSED, currentMedia); + setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position); break; case MediaStatus.PLAYER_STATE_BUFFERING: - setPlayerStatus(playerStatus, currentMedia); + setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ? + PlayerStatus.PREPARING : PlayerStatus.SEEKING, + currentMedia, + currentMedia != null ? currentMedia.getPosition() : INVALID_TIME); break; case MediaStatus.PLAYER_STATE_IDLE: int reason = status.getIdleReason(); switch (reason) { case MediaStatus.IDLE_REASON_CANCELED: - // check if we're already loading something else - if (!updateUI || media == null) { - setPlayerStatus(PlayerStatus.STOPPED, currentMedia); - } else { - updateUI = false; + // Essentially means stopped at the request of a user + callback.onPlaybackEnded(null, true); + setPlayerStatus(PlayerStatus.STOPPED, currentMedia); + if (oldMedia != null) { + if (position >= 0) { + oldMedia.setPosition(position); + } + callback.onPostPlayback(oldMedia, false, false); } - break; + // onPlaybackEnded pretty much takes care of updating the UI + return; case MediaStatus.IDLE_REASON_INTERRUPTED: - // check if we're already loading something else - if (!updateUI || media == null) { - setPlayerStatus(PlayerStatus.PREPARING, currentMedia); - } else { - updateUI = false; + // Means that a request to load a different media was sent + // Not sure if currentMedia already reflects the to be loaded one + if (mediaChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING) { + callback.onPlaybackPause(null, INVALID_TIME); + setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); } + setPlayerStatus(PlayerStatus.PREPARING, currentMedia); break; case MediaStatus.IDLE_REASON_NONE: + // This probably only happens when we connected but no command has been sent yet. setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia); break; case MediaStatus.IDLE_REASON_FINISHED: - boolean playing = playerStatus == PlayerStatus.PLAYING; - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - endPlaybackCall.endPlayback(currentMedia,playing, false, false); - // endPlayback already updates the UI, so no need to trigger it ourselves - updateUI = false; - break; + // This is our onCompletionListener... + if (mediaChanged && currentMedia != null) { + media = currentMedia; + } + endPlayback(false, true, true); + return; case MediaStatus.IDLE_REASON_ERROR: Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode..."); - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_failed_media_error_skipping); - endPlaybackCall.endPlayback(currentMedia, startWhenPrepared.get(), true, false); - // endPlayback already updates the UI, so no need to trigger it ourselves - updateUI = false; + endPlayback(true, true, true); + return; } break; case MediaStatus.PLAYER_STATE_UNKNOWN: - //is this right? - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); + if (playerStatus != PlayerStatus.INDETERMINATE || media != currentMedia) { + setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); + } break; default: - Log.e(TAG, "Remote media state undetermined!"); - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); + Log.wtf(TAG, "Remote media state undetermined!"); } - if (updateUI) { + if (mediaChanged) { callback.onMediaChanged(true); + if (oldMedia != null) { + callback.onPostPlayback(oldMedia, false, currentMedia != null); + } } } @@ -264,12 +303,13 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { if (!CastUtils.isCastable(playable)) { Log.d(TAG, "media provided is not compatible with cast device"); callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable); - try { - playable.loadMetadata(); - } catch (Playable.PlayableException e) { - Log.e(TAG, "Unable to load metadata of playable", e); + Playable nextPlayable = playable; + do { + nextPlayable = callback.getNextInQueue(nextPlayable); + } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable)); + if (nextPlayable != null) { + playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately); } - callback.endPlayback(playable, startWhenPrepared, true, false); return; } @@ -281,19 +321,21 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { return; } else { // set temporarily to pause in order to update list with current position + boolean isPlaying = playerStatus == PlayerStatus.PLAYING; + int position = media.getPosition(); try { - if (castMgr.isRemoteMediaPlaying()) { - setPlayerStatus(PlayerStatus.PAUSED, media); - } + isPlaying = castMgr.isRemoteMediaPlaying(); + position = (int) castMgr.getCurrentMediaPosition(); } catch (TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e); - // this might end up just being pointless if we need to query the remote device for the position - if (playerStatus == PlayerStatus.PLAYING) { - setPlayerStatus(PlayerStatus.PAUSED, media); - } } - smartMarkAsPlayed(media); - + if (isPlaying) { + callback.onPlaybackPause(media, position); + } + if (!media.getIdentifier().equals(playable.getIdentifier())) { + final Playable oldMedia = media; + callback.onPostPlayback(oldMedia, false, true); + } setPlayerStatus(PlayerStatus.INDETERMINATE, null); } @@ -301,7 +343,6 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { this.media = playable; remoteMedia = remoteVersion(playable); - //this.stream = stream; this.mediaType = media.getMediaType(); this.startWhenPrepared.set(startWhenPrepared); setPlayerStatus(PlayerStatus.INITIALIZING, media); @@ -328,8 +369,9 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { media.getPosition(), media.getLastPlayedTime()); castMgr.play(newPosition); + } else { + castMgr.play(); } - castMgr.play(); } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) { Log.e(TAG, "Unable to resume remote playback", e); } @@ -464,8 +506,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { this.startWhenPrepared.set(startWhenPrepared); } - //TODO I believe some parts of the code make the same decision skipping this check, so that - //should be changed as well + // As things are right now, changing the return value of this function is not enough to ensure + // all other components recognize it. @Override public boolean canSetSpeed() { return false; @@ -557,23 +599,67 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { } @Override - public void endPlayback(boolean wasSkipped, boolean switchingPlayers) { + protected Future<?> endPlayback(boolean wasSkipped, boolean shouldContinue, boolean toStoppedState) { Log.d(TAG, "endPlayback() called"); boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - try { - isPlaying = castMgr.isRemoteMediaPlaying(); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Could not determine if media is playing", e); - } - // TODO make sure we stop playback whenever there's no next episode. if (playerStatus != PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.INDETERMINATE, media); } - callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers); + if (media != null && wasSkipped) { + // current position only really matters when we skip + int position = getPosition(); + if (position >= 0) { + media.setPosition(position); + } + } + final Playable currentMedia = media; + Playable nextMedia = null; + if (shouldContinue) { + nextMedia = callback.getNextInQueue(currentMedia); + + boolean playNextEpisode = isPlaying && nextMedia != null && UserPreferences.isFollowQueue(); + if (playNextEpisode) { + Log.d(TAG, "Playback of next episode will start immediately."); + } else if (nextMedia == null){ + Log.d(TAG, "No more episodes available to play"); + } else { + Log.d(TAG, "Loading next episode, but not playing automatically."); + } + + if (nextMedia != null) { + callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode); + // setting media to null signals to playMediaObject() that we're taking care of post-playback processing + media = null; + playMediaObject(nextMedia, false, true /*TODO for now we always stream*/, playNextEpisode, playNextEpisode); + } + } + if (shouldContinue || toStoppedState) { + boolean shouldPostProcess = true; + if (nextMedia == null) { + try { + castMgr.stop(); + shouldPostProcess = false; + } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) { + Log.e(TAG, "Unable to stop playback", e); + callback.onPlaybackEnded(null, true); + stop(); + } + } + if (shouldPostProcess) { + // Otherwise we rely on the chromecast callback to tell us the playback has stopped. + callback.onPostPlayback(currentMedia, !wasSkipped, nextMedia != null); + } + } else if (isPlaying) { + callback.onPlaybackPause(currentMedia, + currentMedia != null ? currentMedia.getPosition() : INVALID_TIME); + } + + FutureTask<?> future = new FutureTask<>(() -> {}, null); + future.run(); + return future; } - @Override - public void stop() { + private void stop() { if (playerStatus == PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.STOPPED, null); } else { @@ -585,8 +671,4 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer { protected boolean shouldLockWifi() { return false; } - - private interface EndPlaybackCall { - boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers); - } } |