diff --git a/app/build.gradle b/app/build.gradle index 407a02b7..bc63fbd1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,8 +158,8 @@ android { // Version code schema (not used): // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3020136 - versionName "4.9.5" + versionCode 3020137 + versionName "4.9.6" def commit = "" try { diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt index e7f71a7b..8c5a46f1 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt @@ -2,8 +2,8 @@ package de.test.podcini.service.playback import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo class CancelablePSMPCallback(private val originalCallback: PSMPCallback) : PSMPCallback { private var isCancelled = false diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt index eee4b1d0..f72b47ac 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt @@ -2,8 +2,8 @@ package de.test.podcini.service.playback import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo open class DefaultPSMPCallback : PSMPCallback { override fun statusChanged(newInfo: PSMPInfo?) { diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt similarity index 96% rename from app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt rename to app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt index 1592b0fb..586da0b6 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceMediaPlayerTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt @@ -3,11 +3,11 @@ package de.test.podcini.service.playback import androidx.test.annotation.UiThreadTest import androidx.test.filters.MediumTest import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.playback.service.LocalPSMP +import ac.mdiq.podcini.playback.service.LocalMediaPlayer import ac.mdiq.podcini.storage.model.feed.* import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo +import ac.mdiq.podcini.playback.base.MediaPlayerBase +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance @@ -31,7 +31,7 @@ import kotlin.concurrent.Volatile * Test class for LocalPSMP */ @MediumTest -class PlaybackServiceMediaPlayerTest { +class MediaPlayerBaseTest { private var PLAYABLE_LOCAL_URL: String? = null private var httpServer: HTTPBin? = null private var playableFileUrl: String? = null @@ -97,7 +97,7 @@ class PlaybackServiceMediaPlayerTest { @UiThreadTest fun testInit() { val c = InstrumentationRegistry.getInstrumentation().targetContext - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, DefaultPSMPCallback()) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, DefaultPSMPCallback()) psmp.shutdown() } @@ -148,7 +148,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, null) psmp.playMediaObject(p, true, false, false) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -190,7 +190,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, null) psmp.playMediaObject(p, true, true, false) @@ -238,7 +238,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, null) psmp.playMediaObject(p, true, false, true) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -287,7 +287,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, null) psmp.playMediaObject(p, true, true, true) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -327,7 +327,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) psmp.playMediaObject(p, false, false, false) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -368,7 +368,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) psmp.playMediaObject(p, false, true, false) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -414,7 +414,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) psmp.playMediaObject(p, false, false, true) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -463,7 +463,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) psmp.playMediaObject(p, false, true, true) val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS) @@ -519,7 +519,7 @@ class PlaybackServiceMediaPlayerTest { if (assertionError == null) assertionError = AssertionFailedError("Unexpected call to shouldStop") } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) if (initialState == PlayerStatus.PLAYING) { psmp.playMediaObject(p, stream, true, true) @@ -623,7 +623,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) { val startWhenPrepared = (initialState != PlayerStatus.PREPARED) psmp.playMediaObject(writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true) @@ -682,7 +682,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) if (initialState == PlayerStatus.INITIALIZED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PAUSED) { val prepareImmediately = (initialState != PlayerStatus.INITIALIZED) @@ -755,7 +755,7 @@ class PlaybackServiceMediaPlayerTest { } } }) - val psmp: PlaybackServiceMediaPlayer = LocalPSMP(c, callback) + val psmp: MediaPlayerBase = LocalMediaPlayer(c, callback) val p = writeTestPlayable(playableFileUrl, PLAYABLE_LOCAL_URL) val prepareImmediately = initialState != PlayerStatus.INITIALIZED val startImmediately = initialState != PlayerStatus.PREPARED diff --git a/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt index 9a151060..26d70a41 100644 --- a/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt +++ b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt @@ -1,15 +1,15 @@ package ac.mdiq.podcini.playback.cast import android.content.Context -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback +import ac.mdiq.podcini.playback.base.MediaPlayerBase +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback /** * Stub implementation of CastPsmp for Free build flavour */ object CastPsmp { @JvmStatic - fun getInstanceIfConnected(context: Context, callback: PSMPCallback): PlaybackServiceMediaPlayer? { + fun getInstanceIfConnected(context: Context, callback: PSMPCallback): MediaPlayerBase? { return null } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt index 9fc52bca..90a57648 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt @@ -4,7 +4,7 @@ import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils.getCurrentPlaybackSpeed import ac.mdiq.podcini.preferences.PlaybackPreferences import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder -import ac.mdiq.podcini.playback.service.PlaybackServiceInterface +import ac.mdiq.podcini.playback.service.PlaybackServiceConstants import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent @@ -69,10 +69,10 @@ abstract class PlaybackController(private val activity: FragmentActivity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED) - activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED) + activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceConstants.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED) } else { activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED)) - activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION)) + activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceConstants.ACTION_PLAYER_NOTIFICATION)) } if (!released) bindToService() @@ -184,14 +184,14 @@ abstract class PlaybackController(private val activity: FragmentActivity) { private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - val type = intent.getIntExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_TYPE, -1) - val code = intent.getIntExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_CODE, -1) + val type = intent.getIntExtra(PlaybackServiceConstants.EXTRA_NOTIFICATION_TYPE, -1) + val code = intent.getIntExtra(PlaybackServiceConstants.EXTRA_NOTIFICATION_CODE, -1) if (code == -1 || type == -1) { Log.d(TAG, "Bad arguments. Won't handle intent") return } when (type) { - PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD -> { + PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD -> { if (playbackService == null && PlaybackService.isRunning) { bindToService() return @@ -199,7 +199,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { mediaInfoLoaded = false queryService() } - PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd() + PlaybackServiceConstants.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd() } } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt index a1c9a92b..fa5df57d 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt @@ -6,7 +6,7 @@ import android.os.Parcelable import androidx.core.content.ContextCompat import androidx.media3.common.util.UnstableApi import ac.mdiq.podcini.playback.service.PlaybackService -import ac.mdiq.podcini.playback.service.PlaybackServiceInterface +import ac.mdiq.podcini.playback.service.PlaybackServiceConstants import ac.mdiq.podcini.storage.model.playback.Playable @UnstableApi @@ -31,8 +31,8 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl val intent: Intent get() { val launchIntent = Intent(context, PlaybackService::class.java) - launchIntent.putExtra(PlaybackServiceInterface.EXTRA_PLAYABLE, media as Parcelable) - launchIntent.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, shouldStreamThisTime) + launchIntent.putExtra(PlaybackServiceConstants.EXTRA_PLAYABLE, media as Parcelable) + launchIntent.putExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME, shouldStreamThisTime) return launchIntent } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt b/app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt similarity index 98% rename from app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt rename to app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index 5bfe3dbe..10b52190 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -20,7 +20,7 @@ import kotlin.concurrent.Volatile * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local * and remote (cast devices) playback. */ -abstract class PlaybackServiceMediaPlayer protected constructor(protected val context: Context, protected val callback: PSMPCallback) { +abstract class MediaPlayerBase protected constructor(protected val context: Context, protected val callback: PSMPCallback) { @Volatile private var oldPlayerStatus: PlayerStatus? = null @@ -377,7 +377,8 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co * Holds information about a PSMP object. */ class PSMPInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?) + companion object { - private const val TAG = "PlaybackSvcMediaPlayer" + private const val TAG = "MediaPlayerBase" } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt deleted file mode 100644 index 539e4e4e..00000000 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt +++ /dev/null @@ -1,344 +0,0 @@ -package ac.mdiq.podcini.playback.service - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.util.config.ClientConfig -import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder -import ac.mdiq.podcini.net.download.service.PodciniHttpClient -import ac.mdiq.podcini.util.NetworkUtils.wasDownloadBlocked -import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.preferences.UserPreferences -import android.content.Context -import android.media.audiofx.LoudnessEnhancer -import android.net.Uri -import android.util.Log -import android.view.SurfaceHolder -import androidx.core.util.Consumer -import androidx.media3.common.* -import androidx.media3.common.Player.* -import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences -import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSourceFactory -import androidx.media3.datasource.HttpDataSource.HttpDataSourceException -import androidx.media3.datasource.okhttp.OkHttpDataSource -import androidx.media3.exoplayer.* -import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.exoplayer.source.ProgressiveMediaSource -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride -import androidx.media3.exoplayer.trackselection.ExoTrackSelection -import androidx.media3.extractor.DefaultExtractorsFactory -import androidx.media3.extractor.mp3.Mp3Extractor -import androidx.media3.ui.DefaultTrackNameProvider -import androidx.media3.ui.TrackNameProvider -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import java.util.concurrent.TimeUnit - - -@UnstableApi -class ExoPlayerWrapper internal constructor(private val context: Context) { - private val bufferUpdateInterval = 5L - - private val bufferingUpdateDisposable: Disposable - private var loudnessEnhancer: LoudnessEnhancer? = null - private var mediaSource: MediaSource? = null - private var audioSeekCompleteListener: Runnable? = null - private var audioCompletionListener: Runnable? = null - private var audioErrorListener: Consumer? = null - private var bufferingUpdateListener: Consumer? = null - - private var playbackParameters: PlaybackParameters - - init { - createPlayer() - playbackParameters = exoPlayer!!.playbackParameters - bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage) - } - } - - private fun createPlayer() { - if (exoPlayer == null) createStaticPlayer(context) - - exoPlayer?.addListener(object : Listener { - override fun onPlaybackStateChanged(playbackState: @State Int) { - when (playbackState) { - STATE_ENDED -> { - exoPlayer?.seekTo(C.TIME_UNSET) - if (audioCompletionListener != null) audioCompletionListener?.run() - } - STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED) - else -> bufferingUpdateListener?.accept(BUFFERING_ENDED) - } - } - - override fun onPlayerError(error: PlaybackException) { - if (wasDownloadBlocked(error)) { - audioErrorListener?.accept(context.getString(R.string.download_error_blocked)) - } else { - var cause = error.cause - if (cause is HttpDataSourceException) { - if (cause.cause != null) cause = cause.cause - } - if (cause != null && "Source error" == cause.message) cause = cause.cause - audioErrorListener?.accept(if (cause != null) cause.message else error.message) - } - } - - override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) { - if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run() - } - - override fun onAudioSessionIdChanged(audioSessionId: Int) { - initLoudnessEnhancer(audioSessionId) - } - }) - - initLoudnessEnhancer(exoPlayer!!.audioSessionId) - } - - val currentPosition: Int - get() = exoPlayer!!.currentPosition.toInt() - - val currentSpeedMultiplier: Float - get() = playbackParameters.speed - - val duration: Int - get() { - if (exoPlayer?.duration == C.TIME_UNSET) return Playable.INVALID_TIME - return exoPlayer!!.duration.toInt() - } - - val isPlaying: Boolean - get() = exoPlayer!!.isPlaying -// get() = exoPlayer!!.playWhenReady - - fun pause() { - exoPlayer?.pause() - } - - @Throws(IllegalStateException::class) - fun prepare() { - if (mediaSource == null) return - exoPlayer?.setMediaSource(mediaSource!!, false) - exoPlayer?.prepare() - } - - fun release() { - bufferingUpdateDisposable.dispose() - - audioSeekCompleteListener = null - audioCompletionListener = null - audioErrorListener = null - bufferingUpdateListener = null - } - - fun reset() { - createPlayer() - } - - @Throws(IllegalStateException::class) - fun seekTo(i: Int) { - exoPlayer?.seekTo(i.toLong()) - audioSeekCompleteListener?.run() - } - - fun setAudioStreamType(i: Int) { - val a = exoPlayer!!.audioAttributes - val b = AudioAttributes.Builder() - b.setContentType(i) - b.setFlags(a.flags) - b.setUsage(a.usage) - exoPlayer?.setAudioAttributes(b.build(), false) - } - - @Throws(IllegalArgumentException::class, IllegalStateException::class) - fun setDataSource(s: String, user: String?, password: String?) { - Log.d(TAG, "setDataSource: $s") - - // Call.Factory callFactory = PodciniHttpClient.getHttpClient(); // Assuming it returns OkHttpClient -// OkHttpDataSource.Factory httpDataSourceFactory = new OkHttpDataSource.Factory(callFactory) -// .setUserAgent(ClientConfig.USER_AGENT); - val httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory) - .setUserAgent(ClientConfig.USER_AGENT) - - if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) { - val requestProperties = HashMap() - requestProperties["Authorization"] = HttpCredentialEncoder.encode(user, password, "ISO-8859-1") - httpDataSourceFactory.setDefaultRequestProperties(requestProperties) - } - val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(context, null, httpDataSourceFactory) - val extractorsFactory = DefaultExtractorsFactory() - extractorsFactory.setConstantBitrateSeekingEnabled(true) - extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA) - val f = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) - val mediaItem = MediaItem.fromUri(Uri.parse(s)) - mediaSource = f.createMediaSource(mediaItem) - } - - @Throws(IllegalArgumentException::class, IllegalStateException::class) - fun setDataSource(s: String) { - setDataSource(s, null, null) - } - - fun setDisplay(sh: SurfaceHolder?) { - exoPlayer?.setVideoSurfaceHolder(sh) - } - - fun setPlaybackParams(speed: Float, skipSilence: Boolean) { - Log.d(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence") - playbackParameters = PlaybackParameters(speed, playbackParameters.pitch) - exoPlayer!!.skipSilenceEnabled = skipSilence - exoPlayer!!.playbackParameters = playbackParameters - } - - fun setVolume(v: Float, v1: Float) { - if (v > 1) { - exoPlayer!!.volume = 1f - loudnessEnhancer?.setEnabled(true) - loudnessEnhancer?.setTargetGain((1000 * (v - 1)).toInt()) - } else { - exoPlayer!!.volume = v - loudnessEnhancer?.setEnabled(false) - } - } - - fun start() { - if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepare() - - exoPlayer?.play() - // Can't set params when paused - so always set it on start in case they changed - exoPlayer!!.playbackParameters = playbackParameters - } - - fun stop() { - exoPlayer?.stop() - } - - val audioTracks: List - get() { - val trackNames: MutableList = ArrayList() - val trackNameProvider: TrackNameProvider = DefaultTrackNameProvider(context.resources) - for (format in formats) { - trackNames.add(trackNameProvider.getTrackName(format)) - } - return trackNames - } - - private val formats: List - get() { - val formats: MutableList = arrayListOf() - val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList() - val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) - for (i in 0 until trackGroups.length) { - formats.add(trackGroups[i].getFormat(0)) - } - return formats - } - - fun setAudioTrack(track: Int) { - val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return - val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) - val override = SelectionOverride(track, 0) - val rendererIndex = audioRendererIndex - val params = trackSelector!!.buildUponParameters().setSelectionOverride(rendererIndex, trackGroups, override) - trackSelector!!.setParameters(params) - } - - private val audioRendererIndex: Int - get() { - for (i in 0 until exoPlayer!!.rendererCount) { - if (exoPlayer?.getRendererType(i) == C.TRACK_TYPE_AUDIO) return i - } - return -1 - } - - val selectedAudioTrack: Int - get() { - val trackSelections = exoPlayer!!.currentTrackSelections - val availableFormats = formats - Log.d(TAG, "selectedAudioTrack called tracks: ${trackSelections.length} formats: ${availableFormats.size}") - for (i in 0 until trackSelections.length) { - val track = trackSelections[i] as? ExoTrackSelection ?: continue - if (availableFormats.contains(track.selectedFormat)) return availableFormats.indexOf(track.selectedFormat) - } - return -1 - } - - fun setOnCompletionListener(audioCompletionListener: Runnable?) { - this.audioCompletionListener = audioCompletionListener - } - - fun setOnSeekCompleteListener(audioSeekCompleteListener: Runnable?) { - this.audioSeekCompleteListener = audioSeekCompleteListener - } - - fun setOnErrorListener(audioErrorListener: Consumer?) { - this.audioErrorListener = audioErrorListener - } - - val videoWidth: Int - get() { - return exoPlayer?.videoFormat?.width ?: 0 - } - - val videoHeight: Int - get() { - return exoPlayer?.videoFormat?.height ?: 0 - } - - fun setOnBufferingUpdateListener(bufferingUpdateListener: Consumer?) { - this.bufferingUpdateListener = bufferingUpdateListener - } - - private fun initLoudnessEnhancer(audioStreamId: Int) { - val newEnhancer = LoudnessEnhancer(audioStreamId) - val oldEnhancer = this.loudnessEnhancer - if (oldEnhancer != null) { - newEnhancer.setEnabled(oldEnhancer.enabled) - if (oldEnhancer.enabled) newEnhancer.setTargetGain(oldEnhancer.targetGain.toInt()) - oldEnhancer.release() - } - - this.loudnessEnhancer = newEnhancer - } - - companion object { - const val BUFFERING_STARTED: Int = -1 - const val BUFFERING_ENDED: Int = -2 - private const val TAG = "ExoPlayerWrapper" - const val ERROR_CODE_OFFSET: Int = 1000 - - private var trackSelector: DefaultTrackSelector? = null - var exoPlayer: ExoPlayer? = null - - fun createStaticPlayer(context: Context) { - val loadControl = DefaultLoadControl.Builder() - loadControl.setBufferDurationsMs(30000, 120000, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) - loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true) - trackSelector = DefaultTrackSelector(context) - val audioOffloadPreferences = AudioOffloadPreferences.Builder() - .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed - .setIsGaplessSupportRequired(true) - .setIsSpeedChangeSupportRequired(true) - .build() - Log.d(TAG, "createPlayer creating exoPlayer_") - - exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context)) - .setTrackSelector(trackSelector!!) - .setLoadControl(loadControl.build()) - .build() - - exoPlayer?.setSeekParameters(SeekParameters.EXACT) - exoPlayer!!.trackSelectionParameters = exoPlayer!!.trackSelectionParameters - .buildUpon() - .setAudioOffloadPreferences(audioOffloadPreferences) - .build() - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt similarity index 60% rename from app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt rename to app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index 1bd076f7..a772ad3e 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -1,10 +1,28 @@ package ac.mdiq.podcini.playback.service +import ac.mdiq.podcini.R +import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils +import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder +import ac.mdiq.podcini.net.download.service.PodciniHttpClient +import ac.mdiq.podcini.playback.base.MediaPlayerBase +import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.playback.base.RewindAfterPauseUtils +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.storage.model.feed.FeedMedia +import ac.mdiq.podcini.storage.model.playback.MediaType +import ac.mdiq.podcini.storage.model.playback.Playable +import ac.mdiq.podcini.util.NetworkUtils.wasDownloadBlocked +import ac.mdiq.podcini.util.config.ClientConfig +import ac.mdiq.podcini.util.event.PlayerErrorEvent +import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent +import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent import android.app.UiModeManager import android.content.Context import android.content.res.Configuration import android.media.AudioManager import android.media.AudioManager.OnAudioFocusChangeListener +import android.media.audiofx.LoudnessEnhancer +import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log @@ -14,18 +32,30 @@ import androidx.core.util.Consumer import androidx.media.AudioAttributesCompat import androidx.media.AudioFocusRequestCompat import androidx.media.AudioManagerCompat +import androidx.media3.common.* +import androidx.media3.common.Player.* +import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils -import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent -import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent -import ac.mdiq.podcini.storage.model.feed.FeedMedia -import ac.mdiq.podcini.storage.model.playback.MediaType -import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer -import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.playback.base.RewindAfterPauseUtils -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.util.event.PlayerErrorEvent +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.HttpDataSource.HttpDataSourceException +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride +import androidx.media3.exoplayer.trackselection.ExoTrackSelection +import androidx.media3.extractor.DefaultExtractorsFactory +import androidx.media3.extractor.mp3.Mp3Extractor +import androidx.media3.ui.DefaultTrackNameProvider +import androidx.media3.ui.TrackNameProvider +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import org.greenrobot.eventbus.EventBus import java.io.File import java.io.IOException @@ -38,16 +68,13 @@ import kotlin.concurrent.Volatile * Manages the MediaPlayer object of the PlaybackService. */ @UnstableApi -class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMediaPlayer(context, callback) { +class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBase(context, callback) { private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager @Volatile private var statusBeforeSeeking: PlayerStatus? = null - @Volatile - private var playerWrapper: ExoPlayerWrapper? = null - @Volatile private var playable: Playable? = null @@ -68,6 +95,107 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia private var isShutDown = false private var seekLatch: CountDownLatch? = null +// from wrapper + private val bufferUpdateInterval = 5L + private val bufferingUpdateDisposable: Disposable + private var mediaSource: MediaSource? = null + private var playbackParameters: PlaybackParameters + + private val formats: List + get() { + val formats: MutableList = arrayListOf() + val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList() + val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) + for (i in 0 until trackGroups.length) { + formats.add(trackGroups[i].getFormat(0)) + } + return formats + } + + private val audioRendererIndex: Int + get() { + for (i in 0 until exoPlayer!!.rendererCount) { + if (exoPlayer?.getRendererType(i) == C.TRACK_TYPE_AUDIO) return i + } + return -1 + } + + private val videoWidth: Int + get() { + return exoPlayer?.videoFormat?.width ?: 0 + } + + private val videoHeight: Int + get() { + return exoPlayer?.videoFormat?.height ?: 0 + } + + private fun setupExoPlayer() { + if (exoPlayer == null) { + if (exoplayerListener != null) exoPlayer?.removeListener(exoplayerListener!!) + createStaticPlayer(context) + } + } + + @Throws(IllegalStateException::class) + fun prepareWR() { + if (mediaSource == null) return + exoPlayer?.setMediaSource(mediaSource!!, false) + exoPlayer?.prepare() + } + + fun release() { + bufferingUpdateDisposable.dispose() + +// exoplayerListener = null + audioSeekCompleteListener = null + audioCompletionListener = null + audioErrorListener = null + bufferingUpdateListener = null + } + + private fun setAudioStreamType(i: Int) { + val a = exoPlayer!!.audioAttributes + val b = AudioAttributes.Builder() + b.setContentType(i) + b.setFlags(a.flags) + b.setUsage(a.usage) + exoPlayer?.setAudioAttributes(b.build(), false) + } + + @Throws(IllegalArgumentException::class, IllegalStateException::class) + fun setDataSource(s: String, user: String?, password: String?) { + Log.d(TAG, "setDataSource: $s") + + // Call.Factory callFactory = PodciniHttpClient.getHttpClient(); // Assuming it returns OkHttpClient +// OkHttpDataSource.Factory httpDataSourceFactory = new OkHttpDataSource.Factory(callFactory) +// .setUserAgent(ClientConfig.USER_AGENT); + val httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory) + .setUserAgent(ClientConfig.USER_AGENT) + + if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) { + val requestProperties = HashMap() + requestProperties["Authorization"] = HttpCredentialEncoder.encode(user, password, "ISO-8859-1") + httpDataSourceFactory.setDefaultRequestProperties(requestProperties) + } + val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(context, null, httpDataSourceFactory) + val extractorsFactory = DefaultExtractorsFactory() + extractorsFactory.setConstantBitrateSeekingEnabled(true) + extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA) + val f = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + val mediaItem = MediaItem.fromUri(Uri.parse(s)) + mediaSource = f.createMediaSource(mediaItem) + } + + fun start() { + if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR() + + exoPlayer?.play() + // Can't set params when paused - so always set it on start in case they changed + exoPlayer!!.playbackParameters = playbackParameters + } + + /** * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will @@ -118,6 +246,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia * @see .playMediaObject */ private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) { + Log.d(TAG, "playMediaObject ${playable.getEpisodeTitle()} $forceReset $stream $startWhenPrepared $prepareImmediately") if (this.playable != null) { if (!forceReset && this.playable!!.getIdentifier() == playable.getIdentifier() && playerStatus == PlayerStatus.PLAYING) { // episode is already playing -> ignore method call @@ -126,7 +255,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } else { // stop playback of this episode if (playerStatus == PlayerStatus.PAUSED || (playerStatus == PlayerStatus.PLAYING) || playerStatus == PlayerStatus.PREPARED) - playerWrapper?.stop() + exoPlayer?.stop() // set temporarily to pause in order to update list with current position if (playerStatus == PlayerStatus.PLAYING) callback.onPlaybackPause(this.playable, getPosition()) @@ -145,7 +274,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia mediaType = this.playable!!.getMediaType() videoSize = null createMediaPlayer() - this@LocalPSMP.startWhenPrepared.set(startWhenPrepared) + this@LocalMediaPlayer.startWhenPrepared.set(startWhenPrepared) setPlayerStatus(PlayerStatus.INITIALIZING, this.playable) try { callback.ensureMediaInfoLoaded(this.playable!!) @@ -157,13 +286,13 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (streamurl != null) { if (playable is FeedMedia && playable.item?.feed?.preferences != null) { val preferences = playable.item!!.feed!!.preferences!! - playerWrapper?.setDataSource(streamurl, preferences.username, preferences.password) - } else playerWrapper?.setDataSource(streamurl) + setDataSource(streamurl, preferences.username, preferences.password) + } else setDataSource(streamurl, null, null) } } else -> { val localMediaurl = this.playable!!.getLocalMediaUrl() - if (localMediaurl != null && File(localMediaurl).canRead()) playerWrapper?.setDataSource(localMediaurl) + if (!localMediaurl.isNullOrEmpty() && File(localMediaurl).canRead()) setDataSource(localMediaurl, null, null) else throw IOException("Unable to read local file $localMediaurl") } } @@ -172,7 +301,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (prepareImmediately) { setPlayerStatus(PlayerStatus.PREPARING, this.playable) - playerWrapper?.prepare() + prepareWR() onPrepared(startWhenPrepared) } } catch (e: IOException) { @@ -208,7 +337,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(playable!!.getPosition(), playable!!.getLastPlayedTime()) seekTo(newPosition) } - playerWrapper?.start() + start() setPlayerStatus(PlayerStatus.PLAYING, playable) pausedBecauseOfTransientAudiofocusLoss = false @@ -232,7 +361,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia releaseWifiLockIfNecessary() if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Pausing playback.") - playerWrapper?.pause() + exoPlayer?.pause() setPlayerStatus(PlayerStatus.PAUSED, playable, getPosition()) if (abandonFocus) { @@ -260,7 +389,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (playerStatus == PlayerStatus.INITIALIZED) { Log.d(TAG, "Preparing media player") setPlayerStatus(PlayerStatus.PREPARING, playable) - playerWrapper?.prepare() + prepareWR() onPrepared(startWhenPrepared.get()) } } @@ -272,7 +401,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia check(playerStatus == PlayerStatus.PREPARING) { "Player is not in PREPARING state" } Log.d(TAG, "Resource prepared") - if (playerWrapper != null && mediaType == MediaType.VIDEO) videoSize = Pair(playerWrapper!!.videoWidth, playerWrapper!!.videoHeight) + if (mediaType == MediaType.VIDEO) videoSize = Pair(videoWidth, videoHeight) if (playable != null) { val pos = playable!!.getPosition() @@ -280,7 +409,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (playable!!.getDuration() <= 0) { Log.d(TAG, "Setting duration of media") - if (playerWrapper != null) playable!!.setDuration(playerWrapper!!.duration) + playable!!.setDuration(if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt()) } } setPlayerStatus(PlayerStatus.PREPARED, playable) @@ -299,8 +428,12 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia releaseWifiLockIfNecessary() when { playable != null -> playMediaObject(playable!!, true, isStreaming, startWhenPrepared.get(), false) - playerWrapper != null -> playerWrapper!!.reset() - else -> Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null") +// TODO: +// playerWrapper != null -> playerWrapper!!.reset() + else -> { + setupExoPlayer() // TODO + Log.d(TAG, "Call to reinit: media and mediaPlayer were null") + } } } @@ -317,8 +450,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (t >= getDuration()) { Log.d(TAG, "Seek reached end of file, skipping to next episode") -// TODO: test - playerWrapper?.seekTo(t) + exoPlayer?.seekTo(t.toLong()) + audioSeekCompleteListener?.run() endPlayback(true, wasSkipped = true, true, toStoppedState = true) // return } @@ -335,7 +468,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia seekLatch = CountDownLatch(1) statusBeforeSeeking = playerStatus setPlayerStatus(PlayerStatus.SEEKING, playable, getPosition()) - playerWrapper?.seekTo(t) + exoPlayer?.seekTo(t.toLong()) + audioSeekCompleteListener?.run() if (statusBeforeSeeking == PlayerStatus.PREPARED) playable?.setPosition(t) try { seekLatch!!.await(3, TimeUnit.SECONDS) @@ -368,9 +502,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia */ override fun getDuration(): Int { var retVal = Playable.INVALID_TIME - if ((playerStatus == PlayerStatus.PLAYING) - || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - if (playerWrapper != null) retVal = playerWrapper!!.duration + if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + retVal = if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt() } if (retVal <= 0 && playable != null && playable!!.getDuration() > 0) retVal = playable!!.getDuration() return retVal @@ -381,10 +514,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia */ override fun getPosition(): Int { var retVal = Playable.INVALID_TIME -// TODO: test - if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { - if (playerWrapper != null) retVal = playerWrapper!!.currentPosition - } + if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt() + if (retVal <= 0 && playable != null && playable!!.getPosition() >= 0) retVal = playable!!.getPosition() return retVal } @@ -402,9 +533,11 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia * This method is executed on an internal executor service. */ override fun setPlaybackParams(speed: Float, skipSilence: Boolean) { - Log.d(TAG, "Playback speed was set to $speed") EventBus.getDefault().post(SpeedChangedEvent(speed)) - playerWrapper?.setPlaybackParams(speed, skipSilence) + Log.d(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence") + playbackParameters = PlaybackParameters(speed, playbackParameters.pitch) + exoPlayer!!.skipSilenceEnabled = skipSilence + exoPlayer!!.playbackParameters = playbackParameters } /** @@ -413,9 +546,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia override fun getPlaybackSpeed(): Float { var retVal = 1f if (playerStatus == PlayerStatus.PLAYING|| playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED - || playerStatus == PlayerStatus.PREPARED) { - if (playerWrapper != null) retVal = playerWrapper!!.currentSpeedMultiplier - } + || playerStatus == PlayerStatus.PREPARED) retVal = playbackParameters.speed + return retVal } @@ -436,7 +568,17 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia volumeRight *= adaptionFactor } } - playerWrapper?.setVolume(volumeLeft, volumeRight) +// playerWrapper?.setVolume(volumeLeft, volumeRight) + + if (volumeLeft > 1) { + exoPlayer!!.volume = 1f + loudnessEnhancer?.setEnabled(true) + loudnessEnhancer?.setTargetGain((1000 * (volumeLeft - 1)).toInt()) + } else { + exoPlayer!!.volume = volumeLeft + loudnessEnhancer?.setEnabled(false) + } + Log.d(TAG, "Media player volume was set to $volumeLeft $volumeRight") } @@ -452,30 +594,29 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia * Releases internally used resources. This method should only be called when the object is not used anymore. */ override fun shutdown() { - if (playerWrapper != null) { - try { - clearMediaPlayerListeners() - if (playerWrapper!!.isPlaying) playerWrapper!!.stop() - } catch (e: Exception) { - e.printStackTrace() - } - playerWrapper!!.release() - playerWrapper = null - playerStatus = PlayerStatus.STOPPED + try { + clearMediaPlayerListeners() +// TODO: should use: exoPlayer!!.playWhenReady ? + if (exoPlayer!!.isPlaying) exoPlayer?.stop() + } catch (e: Exception) { + e.printStackTrace() } + release() + playerStatus = PlayerStatus.STOPPED + isShutDown = true abandonAudioFocus() releaseWifiLockIfNecessary() } override fun setVideoSurface(surface: SurfaceHolder?) { - playerWrapper?.setDisplay(surface) + exoPlayer?.setVideoSurfaceHolder(surface) } override fun resetVideoSurface() { if (mediaType == MediaType.VIDEO) { Log.d(TAG, "Resetting video surface") - playerWrapper?.setDisplay(null) + exoPlayer?.setVideoSurfaceHolder(null) reinit() } else { Log.e(TAG, "Resetting video surface for media of Audio type") @@ -490,8 +631,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia * invalid values. */ override fun getVideoSize(): Pair? { - if (playerWrapper != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO) - videoSize = Pair(playerWrapper!!.videoWidth, playerWrapper!!.videoHeight) + if (playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO) + videoSize = Pair(videoWidth, videoHeight) return videoSize } @@ -510,29 +651,44 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } override fun getAudioTracks(): List { - return playerWrapper?.audioTracks?: listOf() + val trackNames: MutableList = ArrayList() + val trackNameProvider: TrackNameProvider = DefaultTrackNameProvider(context.resources) + for (format in formats) { + trackNames.add(trackNameProvider.getTrackName(format)) + } + return trackNames } override fun setAudioTrack(track: Int) { - if (playerWrapper != null) playerWrapper!!.setAudioTrack(track) + val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return + val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) + val override = SelectionOverride(track, 0) + val rendererIndex = audioRendererIndex + val params = trackSelector!!.buildUponParameters().setSelectionOverride(rendererIndex, trackGroups, override) + trackSelector!!.setParameters(params) } override fun getSelectedAudioTrack(): Int { - return playerWrapper?.selectedAudioTrack?:0 + val trackSelections = exoPlayer!!.currentTrackSelections + val availableFormats = formats + Log.d(TAG, "selectedAudioTrack called tracks: ${trackSelections.length} formats: ${availableFormats.size}") + for (i in 0 until trackSelections.length) { + val track = trackSelections[i] as? ExoTrackSelection ?: continue + if (availableFormats.contains(track.selectedFormat)) return availableFormats.indexOf(track.selectedFormat) + } + return -1 } override fun createMediaPlayer() { - playerWrapper?.release() + release() if (playable == null) { - playerWrapper = null playerStatus = PlayerStatus.STOPPED return } - playerWrapper = ExoPlayerWrapper(context) - playerWrapper!!.setAudioStreamType(AudioManager.STREAM_MUSIC) - setMediaPlayerListeners(playerWrapper) + setAudioStreamType(AudioManager.STREAM_MUSIC) + setMediaPlayerListeners() } private val audioFocusChangeListener = OnAudioFocusChangeListener { focusChange -> @@ -559,7 +715,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Lost audio focus temporarily. Pausing...") - playerWrapper?.pause() // Pause without telling the PlaybackService + exoPlayer?.pause() // Pause without telling the PlaybackService pausedBecauseOfTransientAudiofocusLoss = true audioFocusCanceller.removeCallbacksAndMessages(null) @@ -571,7 +727,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia focusChange == AudioManager.AUDIOFOCUS_GAIN -> { Log.d(TAG, "Gained audio focus") audioFocusCanceller.removeCallbacksAndMessages(null) - if (pausedBecauseOfTransientAudiofocusLoss) playerWrapper?.start() // we paused => play now + if (pausedBecauseOfTransientAudiofocusLoss) start() // we paused => play now else setVolume(1.0f, 1.0f) // we ducked => raise audio level back pausedBecauseOfTransientAudiofocusLoss = false @@ -591,6 +747,14 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia .setOnAudioFocusChangeListener(audioFocusChangeListener) .setWillPauseWhenDucked(true) .build() + + setupExoPlayer() + playbackParameters = exoPlayer!!.playbackParameters + bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage) + } } override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) { @@ -602,10 +766,12 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia val position = getPosition() if (position >= 0) playable?.setPosition(position) - playerWrapper?.reset() - + setupExoPlayer() abandonAudioFocus() + Log.d(TAG, "endPlayback $hasEnded $wasSkipped $shouldContinue $toStoppedState") +// printStackTrace() + val currentMedia = playable var nextMedia: Playable? = null @@ -615,6 +781,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia // Start playback immediately if continuous playback is enabled nextMedia = callback.getNextInQueue(currentMedia) if (nextMedia != null) { + Log.d(TAG, "has nextMedia. call callback.onPlaybackEnded false") callback.onPlaybackEnded(nextMedia.getMediaType(), false) // setting media to null signals to playMediaObject() that // we're taking care of post-playback processing @@ -625,9 +792,10 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia when { shouldContinue || toStoppedState -> { if (nextMedia == null) { + Log.d(TAG, "nextMedia is null. call callback.onPlaybackEnded true") callback.onPlaybackEnded(null, true) playable = null - ExoPlayerWrapper.exoPlayer?.stop() + exoPlayer?.stop() stop() } val hasNext = nextMedia != null @@ -653,30 +821,32 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia return isStreaming } - private fun setMediaPlayerListeners(mp: ExoPlayerWrapper?) { - if (mp == null || playable == null) return + private fun setMediaPlayerListeners() { + if (playable == null) return - mp.setOnCompletionListener(Runnable { endPlayback(hasEnded = true, wasSkipped = false, shouldContinue = true, toStoppedState = true) }) - mp.setOnSeekCompleteListener(Runnable { this.genericSeekCompleteListener() }) - mp.setOnBufferingUpdateListener(Consumer { percent: Int -> + audioCompletionListener = Runnable { + Log.d(TAG, "audioCompletionListener called") + endPlayback(hasEnded = true, wasSkipped = false, shouldContinue = true, toStoppedState = true) + } + audioSeekCompleteListener = Runnable { this.genericSeekCompleteListener() } + bufferingUpdateListener = Consumer { percent: Int -> when (percent) { - ExoPlayerWrapper.BUFFERING_STARTED -> EventBus.getDefault().post(BufferUpdateEvent.started()) - ExoPlayerWrapper.BUFFERING_ENDED -> EventBus.getDefault().post(BufferUpdateEvent.ended()) + BUFFERING_STARTED -> EventBus.getDefault().post(BufferUpdateEvent.started()) + BUFFERING_ENDED -> EventBus.getDefault().post(BufferUpdateEvent.ended()) else -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)) } - }) - mp.setOnErrorListener(Consumer { message: String -> + } + audioErrorListener = Consumer { message: String -> Log.e(TAG, "PlayerErrorEvent: $message") EventBus.getDefault().postSticky(PlayerErrorEvent(message)) - }) + } } private fun clearMediaPlayerListeners() { - if (playerWrapper == null) return - playerWrapper!!.setOnCompletionListener {} - playerWrapper!!.setOnSeekCompleteListener {} - playerWrapper!!.setOnBufferingUpdateListener { } - playerWrapper!!.setOnErrorListener { } + audioCompletionListener = Runnable {} + audioSeekCompleteListener = Runnable {} + bufferingUpdateListener = Consumer { } + audioErrorListener = Consumer {} } private fun genericSeekCompleteListener() { @@ -695,5 +865,107 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia companion object { private const val TAG = "LclPlaybackSvcMPlayer" + +// from wrapper + const val BUFFERING_STARTED: Int = -1 + const val BUFFERING_ENDED: Int = -2 + const val ERROR_CODE_OFFSET: Int = 1000 + + private var trackSelector: DefaultTrackSelector? = null + var exoPlayer: ExoPlayer? = null + + private var exoplayerListener: Listener? = null + private var audioSeekCompleteListener: Runnable? = null + private var audioCompletionListener: Runnable? = null + private var audioErrorListener: Consumer? = null + private var bufferingUpdateListener: Consumer? = null + private var loudnessEnhancer: LoudnessEnhancer? = null + + fun createStaticPlayer(context: Context) { + val loadControl = DefaultLoadControl.Builder() + loadControl.setBufferDurationsMs(30000, 120000, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) + loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true) + trackSelector = DefaultTrackSelector(context) + val audioOffloadPreferences = AudioOffloadPreferences.Builder() + .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed + .setIsGaplessSupportRequired(true) + .setIsSpeedChangeSupportRequired(true) + .build() + Log.d(TAG, "createStaticPlayer creating exoPlayer_") + + exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context)) + .setTrackSelector(trackSelector!!) + .setLoadControl(loadControl.build()) + .build() + + exoPlayer?.setSeekParameters(SeekParameters.EXACT) + exoPlayer!!.trackSelectionParameters = exoPlayer!!.trackSelectionParameters + .buildUpon() + .setAudioOffloadPreferences(audioOffloadPreferences) + .build() + + exoplayerListener = object : Listener { + override fun onPlaybackStateChanged(playbackState: @State Int) { + Log.d(TAG, "onPlaybackStateChanged $playbackState") + when (playbackState) { + STATE_ENDED -> { + exoPlayer?.seekTo(C.TIME_UNSET) + if (audioCompletionListener != null) audioCompletionListener?.run() + } + STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED) + else -> bufferingUpdateListener?.accept(BUFFERING_ENDED) + } + } + + override fun onPlayerError(error: PlaybackException) { + Log.d(TAG, "onPlayerError ${error.message}") + if (wasDownloadBlocked(error)) audioErrorListener?.accept(context.getString(R.string.download_error_blocked)) + else { + var cause = error.cause + if (cause is HttpDataSourceException) { + if (cause.cause != null) cause = cause.cause + } + if (cause != null && "Source error" == cause.message) cause = cause.cause + audioErrorListener?.accept(if (cause != null) cause.message else error.message) + } + } + + override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) { + Log.d(TAG, "onPositionDiscontinuity $oldPosition $newPosition $reason") + if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run() + } + + override fun onAudioSessionIdChanged(audioSessionId: Int) { + Log.d(TAG, "onAudioSessionIdChanged $audioSessionId") + initLoudnessEnhancer(audioSessionId) + } + } + + exoPlayer?.addListener(exoplayerListener!!) + + initLoudnessEnhancer(exoPlayer!!.audioSessionId) + } + + private fun initLoudnessEnhancer(audioStreamId: Int) { + val newEnhancer = LoudnessEnhancer(audioStreamId) + val oldEnhancer = loudnessEnhancer + if (oldEnhancer != null) { + newEnhancer.setEnabled(oldEnhancer.enabled) + if (oldEnhancer.enabled) newEnhancer.setTargetGain(oldEnhancer.targetGain.toInt()) + oldEnhancer.release() + } + + loudnessEnhancer = newEnhancer + } + + fun cleanup() { + exoplayerListener = null + audioSeekCompleteListener = null + audioCompletionListener = null + audioErrorListener = null + bufferingUpdateListener = null + loudnessEnhancer = null + } } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt index 4dafb920..7f50e5fd 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -4,9 +4,9 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.playback.PlayableUtils.saveCurrentPosition import ac.mdiq.podcini.playback.PlaybackServiceStarter -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo +import ac.mdiq.podcini.playback.base.MediaPlayerBase +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback +import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.cast.CastPsmp import ac.mdiq.podcini.playback.cast.CastStateListener @@ -81,9 +81,6 @@ import android.view.SurfaceHolder import android.webkit.URLUtil import android.widget.Toast import androidx.core.app.NotificationCompat -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.Player import androidx.media3.common.Player.STATE_ENDED import androidx.media3.common.Player.STATE_IDLE import androidx.media3.common.util.UnstableApi @@ -112,7 +109,7 @@ import kotlin.math.max */ @UnstableApi class PlaybackService : MediaSessionService() { - private var mediaPlayer: PlaybackServiceMediaPlayer? = null + private var mediaPlayer: MediaPlayerBase? = null private var positionEventTimer: Disposable? = null private lateinit var customMediaNotificationProvider: CustomMediaNotificationProvider @@ -153,10 +150,10 @@ class PlaybackService : MediaSessionService() { if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED) - registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE), RECEIVER_NOT_EXPORTED) + registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE), RECEIVER_NOT_EXPORTED) } else { registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS")) - registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) + registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) } registerReceiver(headsetDisconnected, IntentFilter(Intent.ACTION_HEADSET_PLUG)) @@ -181,8 +178,8 @@ class PlaybackService : MediaSessionService() { customMediaNotificationProvider = CustomMediaNotificationProvider(applicationContext) setMediaNotificationProvider(customMediaNotificationProvider) - if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext) - mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!) + if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext) + mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!) .setCallback(MyCallback()) .setCustomLayout(notificationCustomButtons) .build() @@ -200,7 +197,7 @@ class PlaybackService : MediaSessionService() { mediaPlayer!!.shutdown() } mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback) - if (mediaPlayer == null) mediaPlayer = LocalPSMP(applicationContext, mediaPlayerCallback) // Cast not supported or not connected + if (mediaPlayer == null) mediaPlayer = LocalMediaPlayer(applicationContext, mediaPlayerCallback) // Cast not supported or not connected if (media != null) mediaPlayer!!.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true) isCasting = mediaPlayer!!.isCasting() } @@ -226,13 +223,13 @@ class PlaybackService : MediaSessionService() { castStateListener.destroy() cancelPositionObserver() + LocalMediaPlayer.cleanup() mediaSession?.run { player.release() release() mediaSession = null } - ExoPlayerWrapper.exoPlayer?.release() - ExoPlayerWrapper.exoPlayer = null + LocalMediaPlayer.exoPlayer = null mediaPlayer?.shutdown() unregisterReceiver(autoStateUpdated) @@ -464,13 +461,13 @@ class PlaybackService : MediaSessionService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) -// Log.d(TAG, "OnStartCommand called") val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false - val playable = intent?.getParcelableExtra(PlaybackServiceInterface.EXTRA_PLAYABLE) - Log.d(TAG, "OnStartCommand $keycode $customAction $hardwareButton $playable") + val playable = intent?.getParcelableExtra(PlaybackServiceConstants.EXTRA_PLAYABLE) + + Log.d(TAG, "OnStartCommand flags=$flags startId=$startId $keycode $customAction $hardwareButton ${playable?.getEpisodeTitle()}") if (keycode == -1 && playable == null && customAction == null) { Log.e(TAG, "PlaybackService was started with no arguments") @@ -493,9 +490,9 @@ class PlaybackService : MediaSessionService() { val handled = handleKeycode(keycode, notificationButton) } playable != null -> { - val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false) - val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false) - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) + val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME, false) + val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_ALWAYS, false) + sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, 0) if (allowStreamAlways) isAllowMobileStreaming = true Observable.fromCallable { if (playable is FeedMedia) return@fromCallable DBReader.getFeedMedia(playable.id) @@ -522,7 +519,6 @@ class PlaybackService : MediaSessionService() { private fun skipIntro(playable: Playable) { val item = (playable as? FeedMedia)?.item ?: currentitem ?: return -// val item = currentitem ?: (playable as? FeedMedia)?.item ?: return val feed = item.feed ?: DBReader.getFeed(item.feedId) val preferences = feed?.preferences @@ -548,8 +544,8 @@ class PlaybackService : MediaSessionService() { } val intentAllowThisTime = Intent(originalIntent) - intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME) - intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true) + intentAllowThisTime.setAction(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME) + intentAllowThisTime.putExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME, true) val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) @@ -557,8 +553,8 @@ class PlaybackService : MediaSessionService() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val intentAlwaysAllow = Intent(intentAllowThisTime) - intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS) - intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true) + intentAlwaysAllow.setAction(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_ALWAYS) + intentAlwaysAllow.putExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_ALWAYS, true) val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) @@ -569,8 +565,7 @@ class PlaybackService : MediaSessionService() { .setSmallIcon(R.drawable.ic_notification_stream) .setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title)) .setContentText(getString(R.string.confirm_mobile_streaming_notification_message)) - .setStyle(NotificationCompat.BigTextStyle() - .bigText(getString(R.string.confirm_mobile_streaming_notification_message))) + .setStyle(NotificationCompat.BigTextStyle().bigText(getString(R.string.confirm_mobile_streaming_notification_message))) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntentAllowThisTime) .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime) @@ -679,9 +674,7 @@ class PlaybackService : MediaSessionService() { } private fun startPlayingFromPreferences() { - Observable.fromCallable { - createInstanceFromPreferences(applicationContext) - } + Observable.fromCallable { createInstanceFromPreferences(applicationContext) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -703,9 +696,7 @@ class PlaybackService : MediaSessionService() { return } - if (playable.getIdentifier() != currentlyPlayingFeedMediaId) { - clearCurrentlyPlayingTemporaryPlaybackSpeed() - } + if (playable.getIdentifier() != currentlyPlayingFeedMediaId) clearCurrentlyPlayingTemporaryPlaybackSpeed() mediaPlayer?.playMediaObject(playable, stream, true, true) recreateMediaSessionIfNeeded() @@ -740,7 +731,7 @@ class PlaybackService : MediaSessionService() { } override fun onChapterLoaded(media: Playable?) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) + sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, 0) updateMediaSession(mediaPlayer?.playerStatus) } } @@ -748,7 +739,7 @@ class PlaybackService : MediaSessionService() { private val mediaPlayerCallback: PSMPCallback = object : PSMPCallback { override fun statusChanged(newInfo: PSMPInfo?) { currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN - Log.d(TAG, "statusChanged called") + Log.d(TAG, "statusChanged called ${newInfo?.playerStatus}") // updateMediaSession(newInfo?.playerStatus) if (newInfo != null) { when (newInfo.playerStatus) { @@ -787,7 +778,7 @@ class PlaybackService : MediaSessionService() { setSleepTimer(timerMillis()) EventBus.getDefault().post(MessageEvent(getString(R.string.sleep_timer_enabled_label), { disableSleepTimer() }, getString(R.string.undo))) } - loadQueueForMediaSession() +// loadQueueForMediaSession() } PlayerStatus.ERROR -> writeNoMediaPlaying() else -> {} @@ -808,7 +799,7 @@ class PlaybackService : MediaSessionService() { override fun onMediaChanged(reloadUI: Boolean) { Log.d(TAG, "reloadUI callback reached") - if (reloadUI) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) + if (reloadUI) sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, 0) // updateNotificationAndMediaSession(this@PlaybackService.playable) } @@ -895,12 +886,12 @@ class PlaybackService : MediaSessionService() { } private fun getNextInQueue(currentMedia: Playable?): Playable? { + Log.d(TAG, "getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}") if (currentMedia !is FeedMedia) { Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding") writeNoMediaPlaying() return null } - Log.d(TAG, "getNextInQueue()") if (currentMedia.item == null) currentMedia.setItem(DBReader.getFeedItem(currentMedia.itemId)) val item = currentMedia.item if (item == null) { @@ -911,6 +902,7 @@ class PlaybackService : MediaSessionService() { val nextItem = DBReader.getNextInQueue(item) if (nextItem?.media == null) { + Log.d(TAG, "getNextInQueue nextItem: $nextItem media: ${nextItem?.media}") writeNoMediaPlaying() return null } @@ -934,20 +926,20 @@ class PlaybackService : MediaSessionService() { * Set of instructions to be performed when playback ends. */ private fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { - Log.d(TAG, "Playback ended") + Log.d(TAG, "onPlaybackEnded mediaType: $mediaType stopPlaying: $stopPlaying") clearCurrentlyPlayingTemporaryPlaybackSpeed() if (stopPlaying) { taskManager.cancelPositionSaver() cancelPositionObserver() } if (mediaType == null) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END, 0) + sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_PLAYBACK_END, 0) } else { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, + sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, when { - isCasting -> PlaybackServiceInterface.EXTRA_CODE_CAST - mediaType == MediaType.VIDEO -> PlaybackServiceInterface.EXTRA_CODE_VIDEO - else -> PlaybackServiceInterface.EXTRA_CODE_AUDIO + isCasting -> PlaybackServiceConstants.EXTRA_CODE_CAST + mediaType == MediaType.VIDEO -> PlaybackServiceConstants.EXTRA_CODE_VIDEO + else -> PlaybackServiceConstants.EXTRA_CODE_AUDIO }) } } @@ -977,7 +969,7 @@ class PlaybackService : MediaSessionService() { Log.e(TAG, "Cannot do post-playback processing: media was null") return } - Log.d(TAG, "onPostPlayback(): media=" + playable.getEpisodeTitle()) + Log.d(TAG, "onPostPlayback(): ended=$ended skipped=$skipped playingNext=$playingNext media=${playable.getEpisodeTitle()} ") if (playable !is FeedMedia) { Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia") @@ -1008,6 +1000,7 @@ class PlaybackService : MediaSessionService() { if (item != null) { if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) { + Log.d(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped") // only mark the item as played if we're not keeping it anyways DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && smartMarkAsPlayed)) // don't know if it actually matters to not autodownload when smart mark as played is triggered @@ -1037,9 +1030,9 @@ class PlaybackService : MediaSessionService() { } private fun sendNotificationBroadcast(type: Int, code: Int) { - val intent = Intent(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION) - intent.putExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_TYPE, type) - intent.putExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_CODE, code) + val intent = Intent(PlaybackServiceConstants.ACTION_PLAYER_NOTIFICATION) + intent.putExtra(PlaybackServiceConstants.EXTRA_NOTIFICATION_TYPE, type) + intent.putExtra(PlaybackServiceConstants.EXTRA_NOTIFICATION_CODE, code) intent.setPackage(packageName) sendBroadcast(intent) } @@ -1344,7 +1337,7 @@ class PlaybackService : MediaSessionService() { private val shutdownReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) + if (TextUtils.equals(intent.action, PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)) } } @@ -1548,7 +1541,7 @@ class PlaybackService : MediaSessionService() { positionEventTimer = Observable.interval(POSITION_EVENT_INTERVAL, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { - Log.d(TAG, "setupPositionObserver currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") + Log.d(TAG, "positionEventTimer currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration)) skipEndingIfNecessary() } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceConstants.kt similarity index 96% rename from app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt rename to app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceConstants.kt index e3111fd1..75b8161e 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceConstants.kt @@ -1,6 +1,6 @@ package ac.mdiq.podcini.playback.service -object PlaybackServiceInterface { +object PlaybackServiceConstants { const val EXTRA_PLAYABLE: String = "PlaybackService.PlayableExtra" const val EXTRA_ALLOW_STREAM_THIS_TIME: String = "extra.ac.mdiq.podcini.service.allowStream" const val EXTRA_ALLOW_STREAM_ALWAYS: String = "extra.ac.mdiq.podcini.service.allowStreamAlways" diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt index 61667c81..3519243d 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt @@ -2,17 +2,17 @@ package ac.mdiq.podcini.playback.service import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.model.feed.VolumeAdaptionSetting -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer +import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.PlayerStatus internal class PlaybackVolumeUpdater { - fun updateVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) { + fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) { val playable = mediaPlayer.getPlayable() if (playable is FeedMedia) updateFeedMediaVolumeIfNecessary(mediaPlayer, feedId, volumeAdaptionSetting, playable) } - private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long, + private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia) { if (feedMedia.item?.feed?.id == feedId) { val preferences = feedMedia.item!!.feed!!.preferences @@ -22,7 +22,7 @@ internal class PlaybackVolumeUpdater { } } - private fun forceUpdateVolume(mediaPlayer: PlaybackServiceMediaPlayer) { + private fun forceUpdateVolume(mediaPlayer: MediaPlayerBase) { mediaPlayer.pause(false, false) mediaPlayer.resume() } diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt index 9db74ad6..d66a4bf5 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt @@ -239,7 +239,7 @@ object DBReader { @JvmStatic fun getQueueIDList(): LongList { Log.d(TAG, "getQueueIDList() called") -// printStackTrce() +// printStackTrace() val adapter = getInstance() adapter.open() @@ -524,7 +524,7 @@ object DBReader { * @return The FeedItem next in queue or null if the FeedItem could not be found. */ fun getNextInQueue(item: FeedItem): FeedItem? { - Log.d(TAG, "getNextInQueue() called with: " + "itemId = [" + item.id + "]") + Log.d(TAG, "getNextInQueue() called with: itemId = [${item.id}]") val adapter = getInstance() adapter.open() try { @@ -539,6 +539,7 @@ object DBReader { return nextItem } } catch (e: Exception) { + Log.d(TAG, "getNextInQueue error: ${e.message}") return null } } finally { @@ -640,7 +641,7 @@ object DBReader { fun loadDescriptionOfFeedItem(item: FeedItem) { Log.d(TAG, "loadDescriptionOfFeedItem() called with: item = [$item]") // TODO: need to find out who are often calling this -// printStackTrce() +// printStackTrace() val adapter = getInstance() adapter.open() try { diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt index 83c977f2..cbccaefe 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt @@ -14,7 +14,7 @@ import ac.mdiq.podcini.feed.LocalFeedUpdater.updateFeed import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying -import ac.mdiq.podcini.playback.service.PlaybackServiceInterface +import ac.mdiq.podcini.playback.service.PlaybackServiceConstants import ac.mdiq.podcini.storage.DBReader.getFeed import ac.mdiq.podcini.storage.DBReader.getFeedItem import ac.mdiq.podcini.storage.DBReader.getFeedItemList @@ -43,6 +43,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue +import ac.mdiq.podcini.util.showStackTrace import org.greenrobot.eventbus.EventBus import java.io.File import java.util.* @@ -135,7 +136,7 @@ import java.util.concurrent.TimeUnit if (media.id == currentlyPlayingFeedMediaId) { writeNoMediaPlaying() - sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE) + sendLocalBroadcast(context, PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE) val nm = NotificationManagerCompat.from(context) nm.cancel(R.id.notification_playing) @@ -207,7 +208,7 @@ import java.util.concurrent.TimeUnit if (item.media?.id == currentlyPlayingFeedMediaId) { // Applies to both downloaded and streamed media writeNoMediaPlaying() - sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE) + sendLocalBroadcast(context, PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE) } if (item.feed != null && !item.feed!!.isLocalFeed) { DownloadServiceInterface.get()?.cancel(context, item.media!!) @@ -481,10 +482,10 @@ import java.util.concurrent.TimeUnit return runOnDbThread { removeQueueItemSynchronous(context, performAutoDownload, *itemIds) } } - @UnstableApi private fun removeQueueItemSynchronous(context: Context, - performAutoDownload: Boolean, vararg itemIds: Long) { - Log.d(TAG, "removeQueueItemSynchronous called") + @UnstableApi private fun removeQueueItemSynchronous(context: Context, performAutoDownload: Boolean, vararg itemIds: Long) { + Log.d(TAG, "removeQueueItemSynchronous called $itemIds") if (itemIds.isEmpty()) return + showStackTrace() val adapter = getInstance() adapter.open() @@ -497,6 +498,7 @@ import java.util.concurrent.TimeUnit val position = indexInItemList(queue, itemId) if (position >= 0) { val item = getFeedItem(itemId) + Log.d(TAG, "removing item from queue: ${item?.title}") if (item == null) { Log.e(TAG, "removeQueueItem - item in queue but somehow cannot be loaded. Item ignored. It should never happen. id:$itemId") continue diff --git a/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt b/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt index 4be5c841..84ddbaa9 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt @@ -11,6 +11,7 @@ import ac.mdiq.podcini.storage.model.MediaMetadataRetrieverCompat import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.storage.model.playback.RemoteMedia +import android.util.Log import java.util.* import kotlin.concurrent.Volatile import kotlin.math.max @@ -276,6 +277,7 @@ class FeedMedia : FeedFile, Playable { } override fun onPlaybackPause(context: Context) { + Log.d("FeedMedia", "onPlaybackPause $position $duration") if (position > startPosition) { playedDuration = playedDurationWhenStarted + position - startPosition playedDurationWhenStarted = playedDuration diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt index 1d6a44cf..1233a07b 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt @@ -6,7 +6,7 @@ import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.preferences.PlaybackPreferences import ac.mdiq.podcini.receiver.MediaButtonReceiver -import ac.mdiq.podcini.playback.service.PlaybackServiceInterface +import ac.mdiq.podcini.playback.service.PlaybackServiceConstants import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia @@ -171,7 +171,7 @@ object FeedItemMenuHandler { selectedItem.media?.setPosition(0) if (PlaybackPreferences.currentlyPlayingFeedMediaId == (selectedItem.media?.id ?: "")) { PlaybackPreferences.writeNoMediaPlaying() - IntentUtils.sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE) + IntentUtils.sendLocalBroadcast(context, PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE) } DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, true) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 82bf91b8..9fe3b154 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -157,7 +157,7 @@ class PlayerDetailsFragment : Fragment() { if (item != null) { if (cleanedNotes == null || item!!.description == null || loadedMediaId != media?.getIdentifier()) { Log.d(TAG, "calling load description ${cleanedNotes==null} ${item!!.description==null} ${item!!.media?.getIdentifier()} ${media?.getIdentifier()}") -// printStackTrce() +// printStackTrace() DBReader.loadDescriptionOfFeedItem(item!!) loadedMediaId = media?.getIdentifier() val shownotesCleaner = ShownotesCleaner(context, item?.description ?: "", media?.getDuration()?:0) diff --git a/app/src/main/java/ac/mdiq/podcini/util/StackTrace.kt b/app/src/main/java/ac/mdiq/podcini/util/StackTrace.kt index 6ca77643..f99c726e 100644 --- a/app/src/main/java/ac/mdiq/podcini/util/StackTrace.kt +++ b/app/src/main/java/ac/mdiq/podcini/util/StackTrace.kt @@ -1,8 +1,10 @@ package ac.mdiq.podcini.util -fun printStackTrace() { +import android.util.Log + +fun showStackTrace() { val stackTraceElements = Thread.currentThread().stackTrace stackTraceElements.forEach { element -> - println(element) + Log.d("showStackTrace", element.toString()) } } \ No newline at end of file diff --git a/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt b/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt index b0c257b9..b2499947 100644 --- a/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt +++ b/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt @@ -1,18 +1,16 @@ package ac.mdiq.podcini.playback.cast -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent +import ac.mdiq.podcini.playback.base.MediaPlayerBase +import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.playback.base.RewindAfterPauseUtils.calculatePositionWithRewind import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.storage.model.playback.RemoteMedia -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer -import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.playback.base.RewindAfterPauseUtils.calculatePositionWithRewind -import ac.mdiq.podcini.playback.service.ExoPlayerWrapper +import ac.mdiq.podcini.util.event.PlayerErrorEvent +import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent import android.annotation.SuppressLint import android.content.Context -import android.media.AudioManager import android.util.Log import android.util.Pair import android.view.SurfaceHolder @@ -29,10 +27,10 @@ import kotlin.math.max import kotlin.math.min /** - * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices. + * Implementation of MediaPlayerBase suitable for remote playback on Cast Devices. */ @SuppressLint("VisibleForTests") -class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaPlayer(context, callback) { +class CastPsmp(context: Context, callback: PSMPCallback) : MediaPlayerBase(context, callback) { @Volatile private var media: Playable? @@ -82,38 +80,28 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP } private fun setBuffering(buffering: Boolean) { - if (buffering && isBuffering.compareAndSet(false, true)) { - EventBus.getDefault().post(BufferUpdateEvent.started()) - } else if (!buffering && isBuffering.compareAndSet(true, false)) { - EventBus.getDefault().post(BufferUpdateEvent.ended()) + when { + buffering && isBuffering.compareAndSet(false, true) -> EventBus.getDefault().post(BufferUpdateEvent.started()) + !buffering && isBuffering.compareAndSet(true, false) -> EventBus.getDefault().post(BufferUpdateEvent.ended()) } } private fun localVersion(info: MediaInfo?): Playable? { - if (info == null || info.metadata == null) { - return null - } - if (CastUtils.matches(info, media)) { - return media - } + if (info == null || info.metadata == null) return null + if (CastUtils.matches(info, media)) return media + val streamUrl = info.metadata!!.getString(CastUtils.KEY_STREAM_URL) return if (streamUrl == null) CastUtils.makeRemoteMedia(info) else callback.findMedia(streamUrl) } private fun remoteVersion(playable: Playable?): MediaInfo? { - if (playable == null) { - return null - } - if (CastUtils.matches(remoteMedia, playable)) { - return remoteMedia - } - if (playable is FeedMedia) { - return MediaInfoCreator.from(playable) + return when { + playable == null -> null + CastUtils.matches(remoteMedia, playable) -> remoteMedia + playable is FeedMedia -> MediaInfoCreator.from(playable) + playable is RemoteMedia -> MediaInfoCreator.from(playable) + else -> null } - if (playable is RemoteMedia) { - return MediaInfoCreator.from(playable) - } - return null } private fun onRemoteMediaPlayerStatusUpdated() { @@ -121,9 +109,8 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP if (status == null) { Log.d(TAG, "Received null MediaStatus") return - } else { - Log.d(TAG, "Received remote status/media update. New state=" + status.playerState) - } + } else Log.d(TAG, "Received remote status/media update. New state=" + status.playerState) + var state = status.playerState val oldState = remoteState remoteMedia = status.mediaInfo @@ -137,16 +124,13 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP val oldMedia = media val position = status.streamPosition.toInt() // check for incompatible states - if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED) - && currentMedia == null) { + 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 (stateChanged) remoteState = state if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && state != MediaStatus.PLAYER_STATE_IDLE) { callback.onPlaybackPause(null, Playable.INVALID_TIME) @@ -160,17 +144,16 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP MediaStatus.PLAYER_STATE_PLAYING -> { if (!stateChanged) { //These steps are necessary because they won't be performed by setPlayerStatus() - if (position >= 0) { - currentMedia!!.setPosition(position) - } + if (position >= 0) currentMedia!!.setPosition(position) currentMedia!!.onPlaybackStart() } setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position) } MediaStatus.PLAYER_STATE_PAUSED -> setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position) - MediaStatus.PLAYER_STATE_BUFFERING -> setPlayerStatus(if ((mediaChanged || playerStatus == PlayerStatus.PREPARING) - ) PlayerStatus.PREPARING else PlayerStatus.SEEKING, currentMedia, - currentMedia?.getPosition() ?: Playable.INVALID_TIME) + MediaStatus.PLAYER_STATE_BUFFERING -> setPlayerStatus( + if ((mediaChanged || playerStatus == PlayerStatus.PREPARING)) PlayerStatus.PREPARING + else PlayerStatus.SEEKING, + currentMedia, currentMedia?.getPosition() ?: Playable.INVALID_TIME) MediaStatus.PLAYER_STATE_IDLE -> { val reason = status.idleReason when (reason) { @@ -179,9 +162,7 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP callback.onPlaybackEnded(null, true) setPlayerStatus(PlayerStatus.STOPPED, currentMedia) if (oldMedia != null) { - if (position >= 0) { - oldMedia.setPosition(position) - } + if (position >= 0) oldMedia.setPosition(position) callback.onPostPlayback(oldMedia, ended = false, skipped = false, playingNext = false) } // onPlaybackEnded pretty much takes care of updating the UI @@ -196,19 +177,16 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP } setPlayerStatus(PlayerStatus.PREPARING, currentMedia) } - MediaStatus.IDLE_REASON_NONE -> // This probably only happens when we connected but no command has been sent yet. - setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia) + // This probably only happens when we connected but no command has been sent yet. + MediaStatus.IDLE_REASON_NONE -> setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia) MediaStatus.IDLE_REASON_FINISHED -> { // This is our onCompletionListener... - if (mediaChanged && currentMedia != null) { - media = currentMedia - } + if (mediaChanged && currentMedia != null) media = currentMedia endPlayback(true, wasSkipped = false, shouldContinue = true, toStoppedState = true) return } MediaStatus.IDLE_REASON_ERROR -> { - Log.w(TAG, "Got an error status from the Chromecast. " - + "Skipping, if possible, to the next episode...") + Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...") EventBus.getDefault().post(PlayerErrorEvent("Chromecast error code 1")) endPlayback(false, wasSkipped = false, shouldContinue = true, toStoppedState = true) return @@ -223,9 +201,7 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP } if (mediaChanged) { callback.onMediaChanged(true) - if (oldMedia != null) { - callback.onPostPlayback(oldMedia, ended = false, skipped = false, playingNext = currentMedia != null) - } + if (oldMedia != null) callback.onPostPlayback(oldMedia, ended = false, skipped = false, playingNext = currentMedia != null) } } @@ -243,9 +219,7 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP // setMediaPlayerListeners(mediaPlayer) } - override fun playMediaObject(playable: Playable, stream: Boolean, - startWhenPrepared: Boolean, prepareImmediately: Boolean - ) { + override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) { Log.d(TAG, "playMediaObject() called") playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately) } @@ -257,20 +231,17 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP * * @see .playMediaObject */ - private fun playMediaObject(playable: Playable, forceReset: Boolean, - stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean - ) { + private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) { if (!CastUtils.isCastable(playable, castContext.sessionManager.currentCastSession)) { Log.d(TAG, "media provided is not compatible with cast device") EventBus.getDefault().post(PlayerErrorEvent("Media not compatible with cast device")) var nextPlayable: Playable? = playable do { nextPlayable = callback.getNextInQueue(nextPlayable) - } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable, - castContext.sessionManager.currentCastSession)) - if (nextPlayable != null) { - playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately) - } + } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable, castContext.sessionManager.currentCastSession)) + + if (nextPlayable != null) playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately) + return } @@ -283,9 +254,8 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP // set temporarily to pause in order to update list with current position val isPlaying = remoteMediaClient!!.isPlaying val position = remoteMediaClient.approximateStreamPosition.toInt() - if (isPlaying) { - callback.onPlaybackPause(media, position) - } + if (isPlaying) callback.onPlaybackPause(media, position) + if (media != null && media?.getIdentifier() != playable.getIdentifier()) { val oldMedia: Playable = media!! callback.onPostPlayback(oldMedia, false, skipped = false, playingNext = true) @@ -302,15 +272,11 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP callback.ensureMediaInfoLoaded(media!!) callback.onMediaChanged(true) setPlayerStatus(PlayerStatus.INITIALIZED, media) - if (prepareImmediately) { - prepare() - } + if (prepareImmediately) prepare() } override fun resume() { - val newPosition = calculatePositionWithRewind( - media!!.getPosition(), - media!!.getLastPlayedTime()) + val newPosition = calculatePositionWithRewind(media!!.getPosition(), media!!.getLastPlayedTime()) seekTo(newPosition) remoteMediaClient!!.play() } @@ -324,11 +290,8 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP Log.d(TAG, "Preparing media player") setPlayerStatus(PlayerStatus.PREPARING, media) var position = media!!.getPosition() - if (position > 0) { - position = calculatePositionWithRewind( - position, - media!!.getLastPlayedTime()) - } + if (position > 0) position = calculatePositionWithRewind(position, media!!.getLastPlayedTime()) + remoteMediaClient!!.load(MediaLoadRequestData.Builder() .setMediaInfo(remoteMedia) .setAutoplay(startWhenPrepared.get()) @@ -338,44 +301,30 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP override fun reinit() { Log.d(TAG, "reinit() called") - if (media != null) { - playMediaObject(media!!, true, - stream = false, - startWhenPrepared = startWhenPrepared.get(), - prepareImmediately = false) - } else { - Log.d(TAG, "Call to reinit was ignored: media was null") - } + if (media != null) playMediaObject(media!!, true, stream = false, startWhenPrepared = startWhenPrepared.get(), prepareImmediately = false) + else Log.d(TAG, "Call to reinit was ignored: media was null") } override fun seekTo(t: Int) { Exception("Seeking to $t").printStackTrace() - remoteMediaClient!!.seek(MediaSeekOptions.Builder() - .setPosition(t.toLong()).build()) + remoteMediaClient!!.seek(MediaSeekOptions.Builder().setPosition(t.toLong()).build()) } override fun seekDelta(d: Int) { val position = getPosition() - if (position != Playable.INVALID_TIME) { - seekTo(position + d) - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta") - } + if (position != Playable.INVALID_TIME) seekTo(position + d) + else Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta") } override fun getDuration(): Int { var retVal = remoteMediaClient!!.streamDuration.toInt() - if (retVal == Playable.INVALID_TIME && media != null && media!!.getDuration() > 0) { - retVal = media!!.getDuration() - } + if (retVal == Playable.INVALID_TIME && media != null && media!!.getDuration() > 0) retVal = media!!.getDuration() return retVal } override fun getPosition(): Int { var retVal = remoteMediaClient!!.approximateStreamPosition.toInt() - if (retVal <= 0 && media != null && media!!.getPosition() >= 0) { - retVal = media!!.getPosition() - } + if (retVal <= 0 && media != null && media!!.getPosition() >= 0) retVal = media!!.getPosition() return retVal } @@ -388,8 +337,7 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP } override fun setPlaybackParams(speed: Float, skipSilence: Boolean) { - val playbackRate = max(MediaLoadOptions.PLAYBACK_RATE_MIN, - min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed.toDouble())).toFloat().toDouble() + val playbackRate = max(MediaLoadOptions.PLAYBACK_RATE_MIN, min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed.toDouble())).toFloat().toDouble() remoteMediaClient!!.setPlaybackRate(playbackRate) } @@ -449,20 +397,15 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP return -1 } - override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, - toStoppedState: Boolean - ) { + override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) { Log.d(TAG, "endPlayback() called") val isPlaying = playerStatus == PlayerStatus.PLAYING - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media) - } + if (playerStatus != PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.INDETERMINATE, media) + if (media != null && wasSkipped) { // current position only really matters when we skip val position = getPosition() - if (position >= 0) { - media!!.setPosition(position) - } + if (position >= 0) media!!.setPosition(position) } val currentMedia = media var nextMedia: Playable? = null @@ -470,36 +413,30 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP nextMedia = callback.getNextInQueue(currentMedia) val playNextEpisode = isPlaying && nextMedia != null - 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.") + when { + playNextEpisode -> Log.d(TAG, "Playback of next episode will start immediately.") + 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, - forceReset = false, - stream = true, - startWhenPrepared = playNextEpisode, - prepareImmediately = playNextEpisode) + playMediaObject(nextMedia, forceReset = false, stream = true, startWhenPrepared = playNextEpisode, prepareImmediately = playNextEpisode) } } - if (shouldContinue || toStoppedState) { - if (nextMedia == null) { - remoteMediaClient!!.stop() - // Otherwise we rely on the chromecast callback to tell us the playback has stopped. - callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, false) - } else { - callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, true) + when { + shouldContinue || toStoppedState -> { + if (nextMedia == null) { + remoteMediaClient!!.stop() + // Otherwise we rely on the chromecast callback to tell us the playback has stopped. + callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, false) + } else { + callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, true) + } } - } else if (isPlaying) { - callback.onPlaybackPause(currentMedia, - currentMedia?.getPosition() ?: Playable.INVALID_TIME) + isPlaying -> callback.onPlaybackPause(currentMedia, currentMedia?.getPosition() ?: Playable.INVALID_TIME) } } @@ -514,17 +451,11 @@ class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaP companion object { const val TAG: String = "CastPSMP" - fun getInstanceIfConnected(context: Context, - callback: PSMPCallback - ): PlaybackServiceMediaPlayer? { - if (GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) { - return null - } + fun getInstanceIfConnected(context: Context, callback: PSMPCallback): MediaPlayerBase? { + if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) return null + try { - if (CastContext.getSharedInstance(context).castState == CastState.CONNECTED) { - return CastPsmp(context, callback) - } + if (CastContext.getSharedInstance(context).castState == CastState.CONNECTED) return CastPsmp(context, callback) } catch (e: Exception) { e.printStackTrace() } diff --git a/app/src/test/java/ac/mdiq/podcini/service/playback/PlaybackVolumeUpdaterTest.kt b/app/src/test/java/ac/mdiq/podcini/service/playback/PlaybackVolumeUpdaterTest.kt index 0c808c44..d5956d07 100644 --- a/app/src/test/java/ac/mdiq/podcini/service/playback/PlaybackVolumeUpdaterTest.kt +++ b/app/src/test/java/ac/mdiq/podcini/service/playback/PlaybackVolumeUpdaterTest.kt @@ -2,7 +2,7 @@ package ac.mdiq.podcini.service.playback import ac.mdiq.podcini.storage.model.feed.* import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer +import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.service.PlaybackVolumeUpdater import org.junit.Before @@ -11,11 +11,11 @@ import org.mockito.ArgumentMatchers import org.mockito.Mockito class PlaybackVolumeUpdaterTest { - private var mediaPlayer: PlaybackServiceMediaPlayer? = null + private var mediaPlayer: MediaPlayerBase? = null @Before fun setUp() { - mediaPlayer = Mockito.mock(PlaybackServiceMediaPlayer::class.java) + mediaPlayer = Mockito.mock(MediaPlayerBase::class.java) } @Test diff --git a/changelog.md b/changelog.md index 196490fc..0e17f99f 100644 --- a/changelog.md +++ b/changelog.md @@ -318,4 +318,9 @@ * TTS speed uses playback speed of the feed or 1.0 * on player detailed view, if showing episode home reader content, then "share notes" shares the reader content * fixed bug of not re-playing a finished episode -* fixed (possibly) bug of marking multiple items played when one is finished playing \ No newline at end of file +* fixed (possibly) bug of marking multiple items played when one is finished playing + +## 4.9.6 + +* fixed the nasty bug of marking multiple items played when one is finished playing +* merged PlayerWrapper class into LocalMediaPlayer \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020137.txt b/fastlane/metadata/android/en-US/changelogs/3020137.txt new file mode 100644 index 00000000..452ac85d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020137.txt @@ -0,0 +1,5 @@ + +Version 4.9.6 brings several changes: + +* fixed the nasty bug of marking multiple items played when one is finished playing +* merged PlayerWrapper class into LocalMediaPlayer