From ccea00e4056b85d5be0ebfa898a1ac961c2e3c56 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sun, 26 Feb 2023 16:38:31 +0100 Subject: Remove deprecated media players (#6354) --- app/build.gradle | 1 - .../de/test/antennapod/playback/PlaybackTest.java | 46 +- .../antennapod/dialog/MediaPlayerErrorDialog.java | 9 - .../antennapod/dialog/PlaybackControlsDialog.java | 19 - .../antennapod/preferences/PreferenceUpgrader.java | 3 - app/src/main/res/layout/audio_controls.xml | 6 - .../main/res/layout/external_player_fragment.xml | 1 - app/src/main/res/xml/preferences_playback.xml | 10 - build.gradle | 1 - core/build.gradle | 1 - .../core/service/playback/ExoPlayerWrapper.java | 74 +- .../core/service/playback/LocalPSMP.java | 751 +++++---------------- .../core/service/playback/PlaybackService.java | 8 - .../antennapod/core/util/playback/AudioPlayer.java | 77 --- .../antennapod/core/util/playback/IPlayer.java | 56 -- .../core/util/playback/PlaybackController.java | 12 - .../antennapod/core/util/playback/VideoPlayer.java | 56 -- core/src/main/res/values/arrays.xml | 12 - .../playback/base/PlaybackServiceMediaPlayer.java | 14 +- .../danoeh/antennapod/playback/cast/CastPsmp.java | 18 +- .../storage/preferences/UserPreferences.java | 30 - ui/i18n/src/main/res/values/strings.xml | 11 - 22 files changed, 221 insertions(+), 995 deletions(-) delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java diff --git a/app/build.gradle b/app/build.gradle index 72442427f..47e7a550a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,7 +125,6 @@ dependencies { implementation "com.joanzapata.iconify:android-iconify-fontawesome:$iconifyVersion" implementation "com.joanzapata.iconify:android-iconify-material:$iconifyVersion" implementation 'com.leinardi.android:speed-dial:3.2.0' - implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.skydoves:balloon:1.4.0' implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3' diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java index 426a30bb8..735cff532 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -4,45 +4,38 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.view.KeyEvent; -import androidx.preference.PreferenceManager; import android.view.View; - +import androidx.preference.PreferenceManager; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; - +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.test.antennapod.EspressoTestUtils; +import de.test.antennapod.IgnoreOnCi; +import de.test.antennapod.ui.UITestUtils; import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.playback.PlaybackController; -import de.test.antennapod.EspressoTestUtils; -import de.test.antennapod.IgnoreOnCi; -import de.test.antennapod.ui.UITestUtils; - import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition; @@ -69,31 +62,20 @@ import static org.junit.Assert.assertTrue; */ @LargeTest @IgnoreOnCi -@RunWith(Parameterized.class) public class PlaybackTest { @Rule public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class, false, false); - @Parameterized.Parameter(value = 0) - public String playerToUse; private UITestUtils uiTestUtils; protected Context context; private PlaybackController controller; - @Parameterized.Parameters(name = "{0}") - public static Collection initParameters() { - return Arrays.asList(new Object[][] { { "exoplayer" }, { "builtin" }, { "sonic" } }); - } - @Before public void setUp() throws Exception { context = InstrumentationRegistry.getInstrumentation().getTargetContext(); EspressoTestUtils.clearPreferences(); EspressoTestUtils.clearDatabase(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, playerToUse).apply(); - uiTestUtils = new UITestUtils(context); uiTestUtils.setup(); } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java index ab5dde92d..306400b29 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/MediaPlayerErrorDialog.java @@ -6,11 +6,9 @@ import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.event.PlayerErrorEvent; -import de.danoeh.antennapod.storage.preferences.UserPreferences; public class MediaPlayerErrorDialog { public static void show(Activity activity, PlayerErrorEvent event) { @@ -25,13 +23,6 @@ public class MediaPlayerErrorDialog { errorDialog.setMessage(errorMessage); errorDialog.setPositiveButton(android.R.string.ok, (dialog, which) -> ((MainActivity) activity).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED)); - if (!UserPreferences.useExoplayer()) { - errorDialog.setNeutralButton(R.string.media_player_switch_to_exoplayer, (dialog, which) -> { - UserPreferences.enableExoplayer(); - ((MainActivity) activity).showSnackbarAbovePlayer( - R.string.media_player_switched_to_exoplayer, Snackbar.LENGTH_LONG); - }); - } errorDialog.create().show(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java index cec41fac0..a87dccdf5 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/PlaybackControlsDialog.java @@ -63,31 +63,12 @@ public class PlaybackControlsDialog extends DialogFragment { } private void setupUi() { - final CheckBox stereoToMono = dialog.findViewById(R.id.stereo_to_mono); - stereoToMono.setChecked(UserPreferences.stereoToMono()); - if (controller != null && !controller.canDownmix()) { - stereoToMono.setEnabled(false); - String sonicOnly = getString(R.string.sonic_only); - stereoToMono.setText(getString(R.string.stereo_to_mono) + " [" + sonicOnly + "]"); - } - final CheckBox skipSilence = dialog.findViewById(R.id.skipSilence); skipSilence.setChecked(UserPreferences.isSkipSilence()); - if (!UserPreferences.useExoplayer()) { - skipSilence.setEnabled(false); - String exoplayerOnly = getString(R.string.exoplayer_only); - skipSilence.setText(getString(R.string.pref_skip_silence_title) + " [" + exoplayerOnly + "]"); - } skipSilence.setOnCheckedChangeListener((buttonView, isChecked) -> { UserPreferences.setSkipSilence(isChecked); controller.setSkipSilence(isChecked); }); - stereoToMono.setOnCheckedChangeListener((buttonView, isChecked) -> { - UserPreferences.stereoToMono(isChecked); - if (controller != null) { - controller.setDownmix(isChecked); - } - }); } private void setupAudioTracks() { diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index f38e58e83..875ed347e 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -57,9 +57,6 @@ public class PreferenceUpgrader { } } if (oldVersion < 1070300) { - prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, - UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); - if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { UserPreferences.setAllowMobileAutoDownload(true); } diff --git a/app/src/main/res/layout/audio_controls.xml b/app/src/main/res/layout/audio_controls.xml index dc48006bb..3abb70961 100644 --- a/app/src/main/res/layout/audio_controls.xml +++ b/app/src/main/res/layout/audio_controls.xml @@ -32,12 +32,6 @@ android:layout_height="wrap_content" android:text="@string/pref_skip_silence_title" /> - - diff --git a/app/src/main/res/layout/external_player_fragment.xml b/app/src/main/res/layout/external_player_fragment.xml index 4969215d9..4012595bc 100644 --- a/app/src/main/res/layout/external_player_fragment.xml +++ b/app/src/main/res/layout/external_player_fragment.xml @@ -19,7 +19,6 @@ android:id="@+id/imgvCover" android:layout_width="wrap_content" android:layout_height="match_parent" - android:contentDescription="@string/media_player" android:adjustViewBounds="true" android:cropToPadding="true" android:maxWidth="96dp" diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml index 832f3cfec..b59c9a055 100644 --- a/app/src/main/res/xml/preferences_playback.xml +++ b/app/src/main/res/xml/preferences_playback.xml @@ -111,14 +111,4 @@ android:summary="@string/pref_skip_keeps_episodes_sum" android:title="@string/pref_skip_keeps_episodes_title"/> - - - - diff --git a/build.gradle b/build.gradle index ae624bfdb..d5f24bc56 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,6 @@ project.ext { rxJavaVersion = "2.2.2" iconifyVersion = "2.2.2" exoPlayerVersion = "2.14.2" - audioPlayerVersion = "v2.0.0" // Google Play build wearableSupportVersion = "2.6.0" diff --git a/core/build.gradle b/core/build.gradle index 466b4ea74..a70a1e1c0 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -66,7 +66,6 @@ dependencies { implementation "com.google.android.exoplayer:exoplayer-core:$exoPlayerVersion" implementation "com.google.android.exoplayer:exoplayer-ui:$exoPlayerVersion" implementation "com.google.android.exoplayer:extension-okhttp:$exoPlayerVersion" - implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" // Non-free dependencies: playApi "com.google.android.support:wearable:$wearableSupportVersion" diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index 9856c617e..e0c5da00b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -40,12 +40,10 @@ import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.HttpCredentialEncoder; import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.model.playback.Playable; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; -import org.antennapod.audio.MediaPlayer; import java.util.ArrayList; import java.util.Collections; @@ -53,19 +51,21 @@ import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; -public class ExoPlayerWrapper implements IPlayer { +public class ExoPlayerWrapper { + public static final int BUFFERING_STARTED = -1; + public static final int BUFFERING_ENDED = -2; private static final String TAG = "ExoPlayerWrapper"; public static final int ERROR_CODE_OFFSET = 1000; + private final Context context; private final Disposable bufferingUpdateDisposable; private SimpleExoPlayer exoPlayer; private MediaSource mediaSource; - private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener; - private MediaPlayer.OnCompletionListener audioCompletionListener; + private Runnable audioSeekCompleteListener; + private Runnable audioCompletionListener; private Consumer audioErrorListener; - private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener; + private Consumer bufferingUpdateListener; private PlaybackParameters playbackParameters; - private MediaPlayer.OnInfoListener infoListener; private DefaultTrackSelector trackSelector; ExoPlayerWrapper(Context context) { @@ -76,7 +76,7 @@ public class ExoPlayerWrapper implements IPlayer { .observeOn(AndroidSchedulers.mainThread()) .subscribe(tickNumber -> { if (bufferingUpdateListener != null) { - bufferingUpdateListener.onBufferingUpdate(null, exoPlayer.getBufferedPercentage()); + bufferingUpdateListener.accept(exoPlayer.getBufferedPercentage()); } }); } @@ -97,11 +97,11 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void onPlaybackStateChanged(@Player.State int playbackState) { if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) { - audioCompletionListener.onCompletion(null); - } else if (infoListener != null && playbackState == Player.STATE_BUFFERING) { - infoListener.onInfo(null, android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START, 0); - } else if (infoListener != null) { - infoListener.onInfo(null, android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END, 0); + audioCompletionListener.run(); + } else if (bufferingUpdateListener != null && playbackState == Player.STATE_BUFFERING) { + bufferingUpdateListener.accept(BUFFERING_STARTED); + } else if (bufferingUpdateListener != null) { + bufferingUpdateListener.accept(BUFFERING_ENDED); } } @@ -130,28 +130,20 @@ public class ExoPlayerWrapper implements IPlayer { @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { if (audioSeekCompleteListener != null && reason == Player.DISCONTINUITY_REASON_SEEK) { - audioSeekCompleteListener.onSeekComplete(null); + audioSeekCompleteListener.run(); } } }); } - @Override - public boolean canDownmix() { - return false; - } - - @Override public int getCurrentPosition() { return (int) exoPlayer.getCurrentPosition(); } - @Override public float getCurrentSpeedMultiplier() { return playbackParameters.speed; } - @Override public int getDuration() { if (exoPlayer.getDuration() == C.TIME_UNSET) { return Playable.INVALID_TIME; @@ -159,23 +151,19 @@ public class ExoPlayerWrapper implements IPlayer { return (int) exoPlayer.getDuration(); } - @Override public boolean isPlaying() { return exoPlayer.getPlayWhenReady(); } - @Override public void pause() { exoPlayer.pause(); } - @Override public void prepare() throws IllegalStateException { exoPlayer.setMediaSource(mediaSource, false); exoPlayer.prepare(); } - @Override public void release() { bufferingUpdateDisposable.dispose(); if (exoPlayer != null) { @@ -187,21 +175,18 @@ public class ExoPlayerWrapper implements IPlayer { bufferingUpdateListener = null; } - @Override public void reset() { exoPlayer.release(); createPlayer(); } - @Override public void seekTo(int i) throws IllegalStateException { exoPlayer.seekTo(i); if (audioSeekCompleteListener != null) { - audioSeekCompleteListener.onSeekComplete(null); + audioSeekCompleteListener.run(); } } - @Override public void setAudioStreamType(int i) { AudioAttributes a = exoPlayer.getAudioAttributes(); AudioAttributes.Builder b = new AudioAttributes.Builder(); @@ -235,51 +220,34 @@ public class ExoPlayerWrapper implements IPlayer { mediaSource = f.createMediaSource(mediaItem); } - @Override public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { setDataSource(s, null, null); } - @Override public void setDisplay(SurfaceHolder sh) { exoPlayer.setVideoSurfaceHolder(sh); } - @Override public void setPlaybackParams(float speed, boolean skipSilence) { playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch); exoPlayer.setSkipSilenceEnabled(skipSilence); exoPlayer.setPlaybackParameters(playbackParameters); } - @Override - public void setDownmix(boolean b) { - - } - - @Override public void setVolume(float v, float v1) { exoPlayer.setVolume(v); } - @Override - public void setWakeMode(Context context, int i) { - - } - - @Override public void start() { exoPlayer.play(); // Can't set params when paused - so always set it on start in case they changed exoPlayer.setPlaybackParameters(playbackParameters); } - @Override public void stop() { exoPlayer.stop(); } - @Override public List getAudioTracks() { List trackNames = new ArrayList<>(); TrackNameProvider trackNameProvider = new DefaultTrackNameProvider(context.getResources()); @@ -302,7 +270,6 @@ public class ExoPlayerWrapper implements IPlayer { return formats; } - @Override public void setAudioTrack(int track) { MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); if (trackInfo == null) { @@ -324,7 +291,6 @@ public class ExoPlayerWrapper implements IPlayer { return -1; } - @Override public int getSelectedAudioTrack() { TrackSelectionArray trackSelections = exoPlayer.getCurrentTrackSelections(); List availableFormats = getFormats(); @@ -340,11 +306,11 @@ public class ExoPlayerWrapper implements IPlayer { return -1; } - void setOnCompletionListener(MediaPlayer.OnCompletionListener audioCompletionListener) { + void setOnCompletionListener(Runnable audioCompletionListener) { this.audioCompletionListener = audioCompletionListener; } - void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener) { + void setOnSeekCompleteListener(Runnable audioSeekCompleteListener) { this.audioSeekCompleteListener = audioSeekCompleteListener; } @@ -366,11 +332,7 @@ public class ExoPlayerWrapper implements IPlayer { return exoPlayer.getVideoFormat().height; } - void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener) { + void setOnBufferingUpdateListener(Consumer bufferingUpdateListener) { this.bufferingUpdateListener = bufferingUpdateListener; } - - public void setOnInfoListener(MediaPlayer.OnInfoListener infoListener) { - this.infoListener = infoListener; - } } 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 2945cb475..4ff4472ba 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 @@ -6,47 +6,34 @@ import android.content.res.Configuration; import android.media.AudioManager; import android.os.Handler; import android.os.Looper; -import android.os.PowerManager; -import androidx.annotation.NonNull; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; - +import androidx.annotation.NonNull; import androidx.media.AudioAttributesCompat; import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; +import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.playback.base.PlayerStatus; -import org.antennapod.audio.MediaPlayer; +import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.IOException; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; - -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; -import de.danoeh.antennapod.core.util.playback.AudioPlayer; -import de.danoeh.antennapod.core.util.playback.IPlayer; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.core.util.playback.VideoPlayer; -import org.greenrobot.eventbus.EventBus; /** * Manages the MediaPlayer object of the PlaybackService. @@ -57,7 +44,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private final AudioManager audioManager; private volatile PlayerStatus statusBeforeSeeking; - private volatile IPlayer mediaPlayer; + private volatile ExoPlayerWrapper mediaPlayer; private volatile Playable media; private volatile boolean stream; @@ -67,100 +54,15 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private volatile Pair videoSize; private final AudioFocusRequestCompat audioFocusRequest; private final Handler audioFocusCanceller; - - /** - * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads - * have to wait until these operations have finished. - */ - private final PlayerLock playerLock; - private final PlayerExecutor executor; - private boolean useCallerThread = true; private boolean isShutDown = false; - - private CountDownLatch seekLatch; - /** - * All ExoPlayer methods must be executed on the same thread. - * We use the main application thread. This class allows to - * "fake" an executor that just calls the methods on the - * calling thread instead of submitting to an executor. - * Other players are still executed in a background thread. - */ - private class PlayerExecutor { - private ThreadPoolExecutor threadPool; - - public Future submit(Runnable r) { - if (useCallerThread) { - r.run(); - return new FutureTask(() -> {}, null); - } else { - return threadPool.submit(r); - } - } - - public void shutdown() { - threadPool.shutdown(); - } - } - - /** - * All ExoPlayer methods must be executed on the same thread. - * We use the main application thread. This class allows to - * "fake" a lock that does nothing. A lock is not needed if - * everything is called on the same thread. - * Other players are still executed in a background thread and - * therefore use a real lock. - */ - private class PlayerLock { - private ReentrantLock lock = new ReentrantLock(); - - public void lock() { - if (!useCallerThread) { - lock.lock(); - } - } - - public boolean tryLock(int i, TimeUnit milliseconds) throws InterruptedException { - if (!useCallerThread) { - return lock.tryLock(i, milliseconds); - } - return true; - } - - public boolean tryLock() { - if (!useCallerThread) { - return lock.tryLock(); - } - return true; - } - - public void unlock() { - if (!useCallerThread) { - lock.unlock(); - } - } - - public boolean isHeldByCurrentThread() { - if (!useCallerThread) { - return lock.isHeldByCurrentThread(); - } - return true; - } - } - public LocalPSMP(@NonNull Context context, @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) { super(context, callback); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - this.playerLock = new PlayerLock(); this.startWhenPrepared = new AtomicBoolean(false); audioFocusCanceller = new Handler(Looper.getMainLooper()); - - executor = new PlayerExecutor(); - executor.threadPool = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<>(), - (r, executor) -> Log.d(TAG, "Rejected execution of runnable")); - mediaPlayer = null; statusBeforeSeeking = null; pausedBecauseOfTransientAudiofocusLoss = false; @@ -207,18 +109,12 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Log.d(TAG, "playMediaObject(...)"); - useCallerThread = UserPreferences.useExoplayer(); - executor.submit(() -> { - playerLock.lock(); - try { - playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); - } catch (RuntimeException e) { - e.printStackTrace(); - throw e; - } finally { - playerLock.unlock(); - } - }); + try { + playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } } /** @@ -230,11 +126,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * @see #playMediaObject(Playable, boolean, boolean, boolean) */ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - if (!playerLock.isHeldByCurrentThread()) { - throw new IllegalStateException("method requires playerLock"); - } - - if (media != null) { if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) && playerStatus == PlayerStatus.PLAYING) { @@ -253,7 +144,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (!media.getIdentifier().equals(playable.getIdentifier())) { final Playable oldMedia = media; - executor.submit(() -> callback.onPostPlayback(oldMedia, false, false, true)); + callback.onPostPlayback(oldMedia, false, false, true); } setPlayerStatus(PlayerStatus.INDETERMINATE, null); @@ -313,14 +204,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void resume() { - executor.submit(() -> { - playerLock.lock(); - resumeSync(); - playerLock.unlock(); - }); - } - - private void resumeSync() { if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { int focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest); @@ -336,7 +219,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( media.getPosition(), media.getLastPlayedTime()); - seekToSync(newPosition); + seekTo(newPosition); } mediaPlayer.start(); @@ -363,27 +246,22 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void pause(final boolean abandonFocus, final boolean reinit) { - executor.submit(() -> { - playerLock.lock(); - releaseWifiLockIfNecessary(); - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Pausing playback."); - mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); - - if (abandonFocus) { - abandonAudioFocus(); - pausedBecauseOfTransientAudiofocusLoss = false; - } - if (stream && reinit) { - reinit(); - } - } else { - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); - } + releaseWifiLockIfNecessary(); + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Pausing playback."); + mediaPlayer.pause(); + setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); - playerLock.unlock(); - }); + if (abandonFocus) { + abandonAudioFocus(); + pausedBecauseOfTransientAudiofocusLoss = false; + } + if (stream && reinit) { + reinit(); + } + } else { + Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + } } private void abandonAudioFocus() { @@ -398,50 +276,30 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void prepare() { - executor.submit(() -> { - playerLock.lock(); - - if (playerStatus == PlayerStatus.INITIALIZED) { - Log.d(TAG, "Preparing media player"); - setPlayerStatus(PlayerStatus.PREPARING, media); - try { - mediaPlayer.prepare(); - onPrepared(startWhenPrepared.get()); - } catch (IOException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage())); - } - } - playerLock.unlock(); - - }); + if (playerStatus == PlayerStatus.INITIALIZED) { + Log.d(TAG, "Preparing media player"); + setPlayerStatus(PlayerStatus.PREPARING, media); + mediaPlayer.prepare(); + onPrepared(startWhenPrepared.get()); + } } /** * Called after media player has been prepared. This method is executed on the caller's thread. */ private void onPrepared(final boolean startWhenPrepared) { - playerLock.lock(); - if (playerStatus != PlayerStatus.PREPARING) { - playerLock.unlock(); throw new IllegalStateException("Player is not in PREPARING state"); } - Log.d(TAG, "Resource prepared"); - if (mediaType == MediaType.VIDEO && mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper vp = (ExoPlayerWrapper) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); - } else if(mediaType == MediaType.VIDEO && mediaPlayer instanceof VideoPlayer) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); + if (mediaType == MediaType.VIDEO) { + videoSize = new Pair<>(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight()); } // TODO this call has no effect! if (media.getPosition() > 0) { - seekToSync(media.getPosition()); + seekTo(media.getPosition()); } if (media.getDuration() <= 0) { @@ -451,10 +309,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.PREPARED, media); if (startWhenPrepared) { - resumeSync(); + resume(); } - - playerLock.unlock(); } /** @@ -464,35 +320,28 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void reinit() { - useCallerThread = UserPreferences.useExoplayer(); - executor.submit(() -> { - playerLock.lock(); - Log.d(TAG, "reinit()"); - releaseWifiLockIfNecessary(); - if (media != null) { - playMediaObject(media, true, stream, startWhenPrepared.get(), false); - } else if (mediaPlayer != null) { - mediaPlayer.reset(); - } else { - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); - } - playerLock.unlock(); - }); + Log.d(TAG, "reinit()"); + releaseWifiLockIfNecessary(); + if (media != null) { + playMediaObject(media, true, stream, startWhenPrepared.get(), false); + } else if (mediaPlayer != null) { + mediaPlayer.reset(); + } else { + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + } } - /** * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * - * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 - *

- * This method is executed on the caller's thread. + * Invalid time values (< 0) will be ignored. + *

+ * This method is executed on an internal executor service. */ - private void seekToSync(int t) { + @Override + public void seekTo(int t) { if (t < 0) { t = 0; } - playerLock.lock(); if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED @@ -521,18 +370,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { startWhenPrepared.set(false); prepare(); } - playerLock.unlock(); - } - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - *

- * This method is executed on an internal executor service. - */ - @Override - public void seekTo(final int t) { - executor.submit(() -> seekToSync(t)); } /** @@ -542,17 +379,12 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void seekDelta(final int d) { - executor.submit(() -> { - playerLock.lock(); - int currentPosition = getPosition(); - if (currentPosition != Playable.INVALID_TIME) { - seekToSync(currentPosition + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - - playerLock.unlock(); - }); + int currentPosition = getPosition(); + if (currentPosition != Playable.INVALID_TIME) { + seekTo(currentPosition + d); + } else { + Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); + } } /** @@ -560,10 +392,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public int getDuration() { - if (!playerLock.tryLock()) { - return Playable.INVALID_TIME; - } - int retVal = Playable.INVALID_TIME; if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED @@ -573,8 +401,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (retVal <= 0 && media != null && media.getDuration() > 0) { retVal = media.getDuration(); } - - playerLock.unlock(); return retVal; } @@ -583,14 +409,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public int getPosition() { - try { - if (!playerLock.tryLock(50, TimeUnit.MILLISECONDS)) { - return Playable.INVALID_TIME; - } - } catch (InterruptedException e) { - return Playable.INVALID_TIME; - } - int retVal = Playable.INVALID_TIME; if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { retVal = mediaPlayer.getCurrentPosition(); @@ -598,8 +416,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { if (retVal <= 0 && media != null && media.getPosition() >= 0) { retVal = media.getPosition(); } - - playerLock.unlock(); return retVal; } @@ -613,25 +429,15 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { this.startWhenPrepared.set(startWhenPrepared); } - /** - * Sets the playback speed. - * This method is executed on the caller's thread. - */ - private void setSpeedSyncAndSkipSilence(float speed, boolean skipSilence) { - playerLock.lock(); - Log.d(TAG, "Playback speed was set to " + speed); - EventBus.getDefault().post(new SpeedChangedEvent(speed)); - mediaPlayer.setPlaybackParams(speed, skipSilence); - playerLock.unlock(); - } - /** * Sets the playback speed. * This method is executed on an internal executor service. */ @Override public void setPlaybackParams(final float speed, final boolean skipSilence) { - executor.submit(() -> setSpeedSyncAndSkipSilence(speed, skipSilence)); + Log.d(TAG, "Playback speed was set to " + speed); + EventBus.getDefault().post(new SpeedChangedEvent(speed)); + mediaPlayer.setPlaybackParams(speed, skipSilence); } /** @@ -639,10 +445,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public float getPlaybackSpeed() { - if (!playerLock.tryLock()) { - return 1; - } - float retVal = 1; if ((playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED @@ -650,7 +452,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { || playerStatus == PlayerStatus.PREPARED)) { retVal = mediaPlayer.getCurrentSpeedMultiplier(); } - playerLock.unlock(); return retVal; } @@ -659,16 +460,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * This method is executed on an internal executor service. */ @Override - public void setVolume(final float volumeLeft, float volumeRight) { - executor.submit(() -> setVolumeSync(volumeLeft, volumeRight)); - } - - /** - * Sets the playback volume. - * This method is executed on the caller's thread. - */ - private void setVolumeSync(float volumeLeft, float volumeRight) { - playerLock.lock(); + public void setVolume(float volumeLeft, float volumeRight) { Playable playable = getPlayable(); if (playable instanceof FeedMedia) { FeedMedia feedMedia = (FeedMedia) playable; @@ -680,29 +472,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } mediaPlayer.setVolume(volumeLeft, volumeRight); Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight); - playerLock.unlock(); - } - - /** - * Returns true if the mediaplayer can mix stereo down to mono - */ - @Override - public boolean canDownmix() { - boolean retVal = false; - if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { - retVal = mediaPlayer.canDownmix(); - } - return retVal; - } - - @Override - public void setDownmix(boolean enable) { - playerLock.lock(); - if (media != null && media.getMediaType() == MediaType.AUDIO) { - mediaPlayer.setDownmix(enable); - Log.d(TAG, "Media player downmix was set to " + enable); - } - playerLock.unlock(); } @Override @@ -734,35 +503,26 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { playerStatus = PlayerStatus.STOPPED; } isShutDown = true; - executor.shutdown(); abandonAudioFocus(); releaseWifiLockIfNecessary(); } @Override public void setVideoSurface(final SurfaceHolder surface) { - executor.submit(() -> { - playerLock.lock(); - if (mediaPlayer != null) { - mediaPlayer.setDisplay(surface); - } - playerLock.unlock(); - }); + if (mediaPlayer != null) { + mediaPlayer.setDisplay(surface); + } } @Override public void resetVideoSurface() { - executor.submit(() -> { - playerLock.lock(); - if (mediaType == MediaType.VIDEO) { - Log.d(TAG, "Resetting video surface"); - mediaPlayer.setDisplay(null); - reinit(); - } else { - Log.e(TAG, "Resetting video surface for media of Audio type"); - } - playerLock.unlock(); - }); + if (mediaType == MediaType.VIDEO) { + Log.d(TAG, "Resetting video surface"); + mediaPlayer.setDisplay(null); + reinit(); + } else { + Log.e(TAG, "Resetting video surface for media of Audio type"); + } } /** @@ -774,24 +534,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public Pair getVideoSize() { - if (!playerLock.tryLock()) { - // use cached value if lock can't be aquired - return videoSize; - } - Pair res; - if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { - res = null; - } else if (mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper vp = (ExoPlayerWrapper) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); - res = videoSize; - } else { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); - res = videoSize; + if (mediaPlayer != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO) { + videoSize = new Pair<>(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight()); } - playerLock.unlock(); - return res; + return videoSize; } /** @@ -832,16 +578,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { return; } - if (UserPreferences.useExoplayer()) { - mediaPlayer = new ExoPlayerWrapper(context); - } else if (media.getMediaType() == MediaType.VIDEO) { - mediaPlayer = new VideoPlayer(); - } else { - mediaPlayer = new AudioPlayer(context); - } - + mediaPlayer = new ExoPlayerWrapper(context); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); setMediaPlayerListeners(mediaPlayer); } @@ -858,103 +596,94 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { return; } - executor.submit(() -> { - playerLock.lock(); - if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - Log.d(TAG, "Lost audio focus"); - pause(true, false); - callback.shouldStop(); - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK - && !UserPreferences.shouldPauseForFocusLoss()) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - setVolumeSync(0.25f, 0.25f); - pausedBecauseOfTransientAudiofocusLoss = false; - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT - || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - mediaPlayer.pause(); // Pause without telling the PlaybackService - pausedBecauseOfTransientAudiofocusLoss = true; - - audioFocusCanceller.removeCallbacksAndMessages(null); - audioFocusCanceller.postDelayed(() -> { - if (pausedBecauseOfTransientAudiofocusLoss) { - // Still did not get back the audio focus. Now actually pause. - pause(true, false); - } - }, 10000); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - Log.d(TAG, "Gained audio focus"); - audioFocusCanceller.removeCallbacksAndMessages(null); - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - mediaPlayer.start(); - } else { // we ducked => raise audio level back - setVolumeSync(1.0f, 1.0f); - } + if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK + && !UserPreferences.shouldPauseForFocusLoss()) { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + setVolume(0.25f, 0.25f); pausedBecauseOfTransientAudiofocusLoss = false; } - playerLock.unlock(); - }); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT + || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + mediaPlayer.pause(); // Pause without telling the PlaybackService + pausedBecauseOfTransientAudiofocusLoss = true; + + audioFocusCanceller.removeCallbacksAndMessages(null); + audioFocusCanceller.postDelayed(() -> { + if (pausedBecauseOfTransientAudiofocusLoss) { + // Still did not get back the audio focus. Now actually pause. + pause(true, false); + } + }, 10000); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + Log.d(TAG, "Gained audio focus"); + audioFocusCanceller.removeCallbacksAndMessages(null); + if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now + mediaPlayer.start(); + } else { // we ducked => raise audio level back + setVolume(1.0f, 1.0f); + } + pausedBecauseOfTransientAudiofocusLoss = false; + } } }; @Override - protected Future endPlayback(final boolean hasEnded, final boolean wasSkipped, + protected void endPlayback(final boolean hasEnded, final boolean wasSkipped, final boolean shouldContinue, final boolean toStoppedState) { - useCallerThread = UserPreferences.useExoplayer(); - return executor.submit(() -> { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - - // 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); - } - } + releaseWifiLockIfNecessary(); - if (mediaPlayer != null) { - mediaPlayer.reset(); - } + boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - abandonAudioFocus(); - - 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); - if (nextMedia != null) { - callback.onPlaybackEnded(nextMedia.getMediaType(), false); - // setting media to null signals to playMediaObject() that - // we're taking care of post-playback processing - media = null; - playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), isPlaying, isPlaying); - } + // 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 (shouldContinue || toStoppedState) { - if (nextMedia == null) { - callback.onPlaybackEnded(null, true); - stop(); - } - final boolean hasNext = nextMedia != null; + } - executor.submit(() -> callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)); - } else if (isPlaying) { - callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); + if (mediaPlayer != null) { + mediaPlayer.reset(); + } + + abandonAudioFocus(); + + 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); + if (nextMedia != null) { + callback.onPlaybackEnded(nextMedia.getMediaType(), false); + // setting media to null signals to playMediaObject() that + // we're taking care of post-playback processing + media = null; + playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), isPlaying, isPlaying); } - playerLock.unlock(); - }); + } + if (shouldContinue || toStoppedState) { + if (nextMedia == null) { + callback.onPlaybackEnded(null, true); + stop(); + } + final boolean hasNext = nextMedia != null; + + callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext); + } else if (isPlaying) { + callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); + } } /** @@ -964,165 +693,55 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { * abandoning audio focus have to be done with other methods. */ private void stop() { - executor.submit(() -> { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - if (playerStatus == PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.STOPPED, null); - } else { - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); - } - playerLock.unlock(); + releaseWifiLockIfNecessary(); - }); + if (playerStatus == PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.STOPPED, null); + } else { + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + } } @Override - protected boolean shouldLockWifi(){ + protected boolean shouldLockWifi() { return stream; } - private void setMediaPlayerListeners(IPlayer mp) { + private void setMediaPlayerListeners(ExoPlayerWrapper mp) { if (mp == null || media == null) { return; } - if (mp instanceof VideoPlayer) { - if (media.getMediaType() != MediaType.VIDEO) { - Log.w(TAG, "video player, but media type is " + media.getMediaType()); - } - VideoPlayer vp = (VideoPlayer) mp; - vp.setOnCompletionListener(videoCompletionListener); - vp.setOnSeekCompleteListener(videoSeekCompleteListener); - vp.setOnErrorListener(videoErrorListener); - vp.setOnBufferingUpdateListener(videoBufferingUpdateListener); - vp.setOnInfoListener(videoInfoListener); - } else if (mp instanceof AudioPlayer) { - if (media.getMediaType() != MediaType.AUDIO) { - Log.w(TAG, "audio player, but media type is " + media.getMediaType()); - } - AudioPlayer ap = (AudioPlayer) mp; - ap.setOnCompletionListener(audioCompletionListener); - ap.setOnSeekCompleteListener(audioSeekCompleteListener); - ap.setOnErrorListener(audioErrorListener); - ap.setOnBufferingUpdateListener(audioBufferingUpdateListener); - ap.setOnInfoListener(audioInfoListener); - } else if (mp instanceof ExoPlayerWrapper) { - ExoPlayerWrapper ap = (ExoPlayerWrapper) mp; - ap.setOnCompletionListener(audioCompletionListener); - ap.setOnSeekCompleteListener(audioSeekCompleteListener); - ap.setOnBufferingUpdateListener(audioBufferingUpdateListener); - ap.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message))); - ap.setOnInfoListener(audioInfoListener); - } else { - Log.w(TAG, "Unknown media player: " + mp); - } - } - - private void clearMediaPlayerListeners() { - if (mediaPlayer instanceof VideoPlayer) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - vp.setOnCompletionListener(x -> { }); - vp.setOnSeekCompleteListener(x -> { }); - vp.setOnErrorListener((mediaPlayer, i, i1) -> false); - vp.setOnBufferingUpdateListener((mediaPlayer, i) -> { }); - vp.setOnInfoListener((mediaPlayer, i, i1) -> false); - } else if (mediaPlayer instanceof AudioPlayer) { - AudioPlayer ap = (AudioPlayer) mediaPlayer; - ap.setOnCompletionListener(x -> { }); - ap.setOnSeekCompleteListener(x -> { }); - ap.setOnErrorListener((x, y, z) -> false); - ap.setOnBufferingUpdateListener((arg0, percent) -> { }); - ap.setOnInfoListener((arg0, what, extra) -> false); - } else if (mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer; - ap.setOnCompletionListener(x -> { }); - ap.setOnSeekCompleteListener(x -> { }); - ap.setOnBufferingUpdateListener((arg0, percent) -> { }); - ap.setOnErrorListener(x -> { }); - ap.setOnInfoListener((arg0, what, extra) -> false); - } - } - - private final MediaPlayer.OnCompletionListener audioCompletionListener = - mp -> genericOnCompletion(); - - private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = - mp -> genericOnCompletion(); - - private void genericOnCompletion() { - endPlayback(true, false, true, true); - } - - private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = - (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); - - private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = - (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); - - private final MediaPlayer.OnInfoListener audioInfoListener = - (mp, what, extra) -> genericInfoListener(what); - - private final android.media.MediaPlayer.OnInfoListener videoInfoListener = - (mp, what, extra) -> genericInfoListener(what); - - private boolean genericInfoListener(int what) { - switch (what) { - case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START: + mp.setOnCompletionListener(() -> endPlayback(true, false, true, true)); + mp.setOnSeekCompleteListener(this::genericSeekCompleteListener); + mp.setOnBufferingUpdateListener(percent -> { + if (percent == ExoPlayerWrapper.BUFFERING_STARTED) { EventBus.getDefault().post(BufferUpdateEvent.started()); - return true; - case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END: + } else if (percent == ExoPlayerWrapper.BUFFERING_ENDED) { EventBus.getDefault().post(BufferUpdateEvent.ended()); - return true; - default: - return true; - } + } else { + EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); + } + }); + mp.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message))); } - private final MediaPlayer.OnErrorListener audioErrorListener = - (mp, what, extra) -> { - if(mp != null && mp.canFallback()) { - mp.fallback(); - return true; - } else { - return genericOnError(mp, what, extra); - } - }; - - private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError; - - private boolean genericOnError(Object inObj, int what, int extra) { - EventBus.getDefault().postSticky(new PlayerErrorEvent(MediaPlayerError.getErrorString(context, what))); - return true; + private void clearMediaPlayerListeners() { + mediaPlayer.setOnCompletionListener(() -> { }); + mediaPlayer.setOnSeekCompleteListener(() -> { }); + mediaPlayer.setOnBufferingUpdateListener(percent -> { }); + mediaPlayer.setOnErrorListener(x -> { }); } - private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = - mp -> genericSeekCompleteListener(); - - private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = - mp -> genericSeekCompleteListener(); - private void genericSeekCompleteListener() { Log.d(TAG, "genericSeekCompleteListener"); if (seekLatch != null) { seekLatch.countDown(); } - - Runnable r = () -> { - playerLock.lock(); - if (playerStatus == PlayerStatus.PLAYING) { - callback.onPlaybackStart(media, getPosition()); - } - if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media, getPosition()); - } - playerLock.unlock(); - }; - - if (useCallerThread) { - r.run(); - } else { - executor.submit(r); + if (playerStatus == PlayerStatus.PLAYING) { + callback.onPlaybackStart(media, getPosition()); + } + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media, getPosition()); } } 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 2ab0eb138..62ddf969c 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 @@ -1578,14 +1578,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { return mediaPlayer.getPlaybackSpeed(); } - public boolean canDownmix() { - return mediaPlayer.canDownmix(); - } - - public void setDownmix(boolean enable) { - mediaPlayer.setDownmix(enable); - } - public boolean isStartWhenPrepared() { return mediaPlayer.isStartWhenPrepared(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java deleted file mode 100644 index 4f6b2ce3a..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import androidx.preference.PreferenceManager; -import android.util.Log; -import android.view.SurfaceHolder; - -import de.danoeh.antennapod.core.ClientConfig; -import org.antennapod.audio.MediaPlayer; - -import de.danoeh.antennapod.storage.preferences.UserPreferences; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public class AudioPlayer extends MediaPlayer implements IPlayer { - private static final String TAG = "AudioPlayer"; - - public AudioPlayer(Context context) { - super(context, true, ClientConfig.USER_AGENT); - PreferenceManager.getDefaultSharedPreferences(context) - .registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> { - if (UserPreferences.PREF_MEDIA_PLAYER.equals(key)) { - checkMpi(); - } - }); - } - - @Override - public void setDisplay(SurfaceHolder sh) { - if (sh != null) { - Log.e(TAG, "Setting display not supported in Audio Player"); - throw new UnsupportedOperationException("Setting display not supported in Audio Player"); - } - } - - @Override - public void setPlaybackParams(float speed, boolean skipSilence) { - if (canSetSpeed()) { - try { - setPlaybackSpeed(speed); - } catch (Exception e) { - e.printStackTrace(); - } - } - //Default player does not support silence skipping - } - - @Override - protected boolean useSonic() { - return UserPreferences.useSonic(); - } - - @Override - protected boolean downmix() { - return UserPreferences.stereoToMono(); - } - - public List getAudioTracks() { - return Collections.emptyList(); - } - - @Override - public void setAudioTrack(int track) { - } - - @Override - public int getSelectedAudioTrack() { - return -1; - } - - @Override - public void setDataSource(String streamUrl, String username, String password) throws IOException { - setDataSource(streamUrl); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java deleted file mode 100644 index c726c5b5e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import android.view.SurfaceHolder; - -import java.io.IOException; -import java.util.List; - -public interface IPlayer { - boolean canDownmix(); - - int getCurrentPosition(); - - float getCurrentSpeedMultiplier(); - - int getDuration(); - - boolean isPlaying(); - - void pause(); - - void prepare() throws IllegalStateException, IOException; - - void release(); - - void reset(); - - void seekTo(int msec); - - void setAudioStreamType(int streamtype); - - void setDataSource(String path) throws IllegalStateException, IOException, - IllegalArgumentException, SecurityException; - - void setDataSource(String streamUrl, String username, String password) throws IOException; - - void setDisplay(SurfaceHolder sh); - - void setPlaybackParams(float speed, boolean skipSilence); - - void setDownmix(boolean enable); - - void setVolume(float left, float right); - - void start(); - - void stop(); - - void setWakeMode(Context context, int mode); - - List getAudioTracks(); - - void setAudioTrack(int track); - - int getSelectedAudioTrack(); -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index c3b103501..2274dcf83 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -21,7 +21,6 @@ import de.danoeh.antennapod.event.playback.SpeedChangedEvent; import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; @@ -420,17 +419,6 @@ public abstract class PlaybackController { } } - public boolean canDownmix() { - return (playbackService != null && playbackService.canDownmix()) - || UserPreferences.useSonic(); - } - - public void setDownmix(boolean enable) { - if (playbackService != null) { - playbackService.setDownmix(enable); - } - } - public List getAudioTracks() { if (playbackService == null) { return Collections.emptyList(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java deleted file mode 100644 index ecf47f8ae..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.media.MediaPlayer; -import android.util.Log; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public class VideoPlayer extends MediaPlayer implements IPlayer { - private static final String TAG = "VideoPlayer"; - - @Override - public boolean canDownmix() { - return false; - } - - @Override - public float getCurrentSpeedMultiplier() { - return 1; - } - - @Override - public void setPlaybackParams(float speed, boolean skipSilence) { - //Ignore this for non ExoPlayer implementations - } - - @Override - public void setDownmix(boolean b) { - Log.e(TAG, "Setting downmix unsupported in video player"); - throw new UnsupportedOperationException("Setting downmix unsupported in video player"); - } - - @Override - public void setVideoScalingMode(int mode) { - super.setVideoScalingMode(mode); - } - - public List getAudioTracks() { - return Collections.emptyList(); - } - - @Override - public void setAudioTrack(int track) { - } - - @Override - public int getSelectedAudioTrack() { - return -1; - } - - @Override - public void setDataSource(String streamUrl, String username, String password) throws IOException { - setDataSource(streamUrl); - } -} diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 9d8065b86..39f62a5d7 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -216,18 +216,6 @@ DownloadsSection - - @string/media_player_exoplayer_recommended - @string/media_player_builtin - @string/media_player_sonic - - - - exoplayer - builtin - sonic - - @string/sort_date_new_old diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java index a5b6df41e..ef15cef1d 100644 --- a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java +++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java @@ -9,7 +9,6 @@ import android.util.Pair; import android.view.SurfaceHolder; import java.util.List; -import java.util.concurrent.Future; import androidx.annotation.Nullable; import de.danoeh.antennapod.model.playback.MediaType; @@ -158,13 +157,6 @@ public abstract class PlaybackServiceMediaPlayer { */ public abstract void setVolume(float volumeLeft, float volumeRight); - /** - * Returns true if the mediaplayer can mix stereo down to mono - */ - public abstract boolean canDownmix(); - - public abstract void setDownmix(boolean enable); - public abstract MediaType getCurrentMediaType(); public abstract boolean isStreaming(); @@ -232,8 +224,8 @@ public abstract class PlaybackServiceMediaPlayer { * * @see #endPlayback(boolean, boolean, boolean, boolean) */ - public Future stopPlayback(boolean toStoppedState) { - return endPlayback(false, false, false, toStoppedState); + public void stopPlayback(boolean toStoppedState) { + endPlayback(false, false, false, toStoppedState); } /** @@ -262,7 +254,7 @@ public abstract class PlaybackServiceMediaPlayer { * * @return a Future, just for the purpose of tracking its execution. */ - protected abstract Future endPlayback(boolean hasEnded, boolean wasSkipped, + protected abstract void endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, boolean toStoppedState); /** diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java index 0858d4fc9..7562f9806 100644 --- a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java +++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java @@ -8,8 +8,6 @@ import android.view.SurfaceHolder; import java.util.Collections; import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; import androidx.annotation.Nullable; @@ -430,16 +428,6 @@ public class CastPsmp extends PlaybackServiceMediaPlayer { remoteMediaClient.setStreamVolume(volumeLeft); } - @Override - public boolean canDownmix() { - return false; - } - - @Override - public void setDownmix(boolean enable) { - throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player"); - } - @Override public MediaType getCurrentMediaType() { return mediaType; @@ -497,7 +485,7 @@ public class CastPsmp extends PlaybackServiceMediaPlayer { } @Override - protected Future endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, + protected void endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, boolean toStoppedState) { Log.d(TAG, "endPlayback() called"); boolean isPlaying = playerStatus == PlayerStatus.PLAYING; @@ -544,10 +532,6 @@ public class CastPsmp extends PlaybackServiceMediaPlayer { callback.onPlaybackPause(currentMedia, currentMedia != null ? currentMedia.getPosition() : Playable.INVALID_TIME); } - - FutureTask future = new FutureTask<>(() -> { }, null); - future.run(); - return future; } @Override diff --git a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java index 09835a528..93792121b 100644 --- a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java +++ b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java @@ -112,11 +112,8 @@ public class UserPreferences { // Other private static final String PREF_DATA_FOLDER = "prefDataFolder"; public static final String PREF_DELETE_REMOVES_FROM_QUEUE = "prefDeleteRemovesFromQueue"; - public static final String PREF_USAGE_COUNTING_DATE = "prefUsageCounting"; // Mediaplayer - public static final String PREF_MEDIA_PLAYER = "prefMediaPlayer"; - public static final String PREF_MEDIA_PLAYER_EXOPLAYER = "exoplayer"; private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; private static final String PREF_VIDEO_PLAYBACK_SPEED = "prefVideoPlaybackSpeed"; public static final String PREF_PLAYBACK_SKIP_SILENCE = "prefSkipSilence"; @@ -125,7 +122,6 @@ public class UserPreferences { private static final String PREF_QUEUE_LOCKED = "prefQueueLocked"; // Experimental - private static final String PREF_STEREO_TO_MONO = "PrefStereoToMono"; public static final int EPISODE_CLEANUP_QUEUE = -1; public static final int EPISODE_CLEANUP_NULL = -2; public static final int EPISODE_CLEANUP_EXCEPT_FAVORITE = -3; @@ -780,32 +776,6 @@ public class UserPreferences { return Arrays.asList(1.0f, 1.25f, 1.5f); } - public static String getMediaPlayer() { - return prefs.getString(PREF_MEDIA_PLAYER, PREF_MEDIA_PLAYER_EXOPLAYER); - } - - public static boolean useSonic() { - return getMediaPlayer().equals("sonic"); - } - - public static boolean useExoplayer() { - return getMediaPlayer().equals(PREF_MEDIA_PLAYER_EXOPLAYER); - } - - public static void enableExoplayer() { - prefs.edit().putString(PREF_MEDIA_PLAYER, PREF_MEDIA_PLAYER_EXOPLAYER).apply(); - } - - public static boolean stereoToMono() { - return prefs.getBoolean(PREF_STEREO_TO_MONO, false); - } - - public static void stereoToMono(boolean enable) { - prefs.edit() - .putBoolean(PREF_STEREO_TO_MONO, enable) - .apply(); - } - public static int getEpisodeCleanupValue() { return Integer.parseInt(prefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)); } diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index 84eb45cb8..f9b61de08 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -385,7 +385,6 @@ Search… No results Clear history - Media player Episode Cleanup Episodes that should be eligible for removal if Auto Download needs space for new episodes Pause playback when headphones or bluetooth are disconnected @@ -500,19 +499,12 @@ Open bug tracker Copy to clipboard Copied to clipboard - Experimental - Select which media player to use to play files Current value: %1$s Proxy Set a network proxy No web browser found. Enqueue Downloaded Add downloaded episodes to the queue - Built-in Android player (deprecated) - Sonic Media Player (deprecated) - ExoPlayer (recommended) - Switch to ExoPlayer - Switched to ExoPlayer. Skip Silence in Audio Behavior Default Page @@ -800,9 +792,6 @@ Audio controls Playback Speed Audio Effects - Downmix: Stereo to mono - Sonic only - ExoPlayer only Switch to audio only -- cgit v1.2.3