mirror of
https://github.com/XilinJia/Podcini.git
synced 2025-02-07 15:08:40 +01:00
4.9.6 commit
This commit is contained in:
parent
45e5fbef88
commit
e9011429c6
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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?) {
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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<String>? = null
|
||||
private var bufferingUpdateListener: Consumer<Int>? = 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<String, String>()
|
||||
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<String>
|
||||
get() {
|
||||
val trackNames: MutableList<String> = ArrayList()
|
||||
val trackNameProvider: TrackNameProvider = DefaultTrackNameProvider(context.resources)
|
||||
for (format in formats) {
|
||||
trackNames.add(trackNameProvider.getTrackName(format))
|
||||
}
|
||||
return trackNames
|
||||
}
|
||||
|
||||
private val formats: List<Format>
|
||||
get() {
|
||||
val formats: MutableList<Format> = 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<String>?) {
|
||||
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<Int>?) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Format>
|
||||
get() {
|
||||
val formats: MutableList<Format> = 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<String, String>()
|
||||
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<Int, Int>? {
|
||||
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<String> {
|
||||
return playerWrapper?.audioTracks?: listOf()
|
||||
val trackNames: MutableList<String> = 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<Int> { 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<String> { 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<Int> { }
|
||||
audioErrorListener = Consumer<String> {}
|
||||
}
|
||||
|
||||
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<String>? = null
|
||||
private var bufferingUpdateListener: Consumer<Int>? = 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
|
||||
Log.d(TAG, "OnStartCommand $keycode $customAction $hardwareButton $playable")
|
||||
val playable = intent?.getParcelableExtra<Playable>(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()
|
||||
}
|
||||
|
@ -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"
|
@ -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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
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 (CastUtils.matches(remoteMedia, playable)) {
|
||||
return remoteMedia
|
||||
}
|
||||
if (playable is FeedMedia) {
|
||||
return MediaInfoCreator.from(playable)
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
* 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
|
5
fastlane/metadata/android/en-US/changelogs/3020137.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/3020137.txt
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user