4.9.6 commit

This commit is contained in:
Xilin Jia 2024-04-27 17:53:38 +01:00
parent 45e5fbef88
commit e9011429c6
23 changed files with 557 additions and 687 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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