Merge pull request #2010 from domingos86/chromecast-issue-340-new-mp-protocol

Changes to media player protocol
This commit is contained in:
Martin Fietz 2016-08-04 22:14:25 +02:00 committed by GitHub
commit 606c924f3c
11 changed files with 640 additions and 857 deletions

View File

@ -1,6 +1,8 @@
package de.test.antennapod.service.playback;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.test.InstrumentationTestCase;
import junit.framework.AssertionFailedError;
@ -20,6 +22,7 @@ import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.core.service.playback.LocalPSMP;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
@ -112,7 +115,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testInit() {
final Context c = getInstrumentation().getTargetContext();
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, defaultCallback);
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, new DefaultPSMPCallback());
psmp.shutdown();
}
@ -138,7 +141,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectStreamNoStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -159,47 +162,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
@ -217,7 +179,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectStreamStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -238,46 +200,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
@ -296,7 +218,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectStreamNoStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(4);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -320,46 +242,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
@ -376,7 +258,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectStreamStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(5);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -403,47 +285,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
@ -459,7 +300,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectLocalNoStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -480,47 +321,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -537,7 +337,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectLocalStartNoPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -558,46 +358,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -614,7 +374,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectLocalNoStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(4);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -638,46 +398,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = e;
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -693,7 +413,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testPlayMediaObjectLocalStartPrepare() throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final CountDownLatch countDownLatch = new CountDownLatch(5);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
@ -721,46 +441,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
countDownLatch.countDown();
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -773,58 +453,12 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
psmp.shutdown();
}
private final PlaybackServiceMediaPlayer.PSMPCallback defaultCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) { return false; }
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
private void pauseTestSkeleton(final PlayerStatus initialState, final boolean stream, final boolean abandonAudioFocus, final boolean reinit, long timeoutSeconds) throws InterruptedException {
final Context c = getInstrumentation().getTargetContext();
final int latchCount = (stream && reinit) ? 2 : 1;
final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
@ -863,42 +497,12 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = new AssertionFailedError("Unexpected call to shouldStop");
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
if (assertionError == null)
assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -955,7 +559,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
(initialState == PlayerStatus.PREPARED) ? 1 : 0;
final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
@ -973,36 +577,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
if (assertionError == null) {
@ -1010,11 +584,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) {
@ -1048,7 +617,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final Context c = getInstrumentation().getTargetContext();
final int latchCount = 1;
final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
@ -1062,37 +631,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
countDownLatch.countDown();
}
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
@ -1101,11 +639,6 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -1153,7 +686,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final Context c = getInstrumentation().getTargetContext();
final int latchCount = 2;
final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
PlaybackServiceMediaPlayer.PSMPCallback callback = new DefaultPSMPCallback() {
@Override
public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
@ -1169,47 +702,12 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
if (assertionError == null)
assertionError = new AssertionFailedError("Unexpected call to onMediaPlayerError");
return false;
}
@Override
public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
@ -1248,4 +746,71 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
super("Unexpected state change: " + status);
}
}
private class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
@Override
public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
}
@Override
public void shouldStop() {
}
@Override
public void playbackSpeedChanged(float s) {
}
@Override
public void setSpeedAbilityChanged() {
}
@Override
public void onBufferingUpdate(int percent) {
}
@Override
public void onMediaChanged(boolean reloadUI) {
}
@Override
public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
return false;
}
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
return false;
}
@Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean playingNext) {
}
@Override
public void onPlaybackStart(@NonNull Playable playable, int position) {
}
@Override
public void onPlaybackPause(Playable playable, int position) {
}
@Override
public Playable getNextInQueue(Playable currentMedia) {
return null;
}
@Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
}
}
}

View File

@ -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() {
@Override
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

View File

@ -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();
});

View File

@ -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();
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");
}
boolean isInQueue = false;
FeedItem nextItem = null;
if (ended || smartMarkAsPlayed) {
media.onPlaybackCompleted(getApplicationContext());
} else {
media.onPlaybackPause(getApplicationContext());
}
if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) {
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
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

View File

@ -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;
@ -129,13 +127,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.
*/
@ -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);
}
/**

View File

@ -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) {
}

View File

@ -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

View File

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

View File

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

View File

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

View File

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