diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 596168f8..c285294c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -37,6 +37,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.util.AppUpgradeHandler +import com.github.damontecres.wholphin.util.PlaybackLifecycleObserver import com.github.damontecres.wholphin.util.ServerEventListener import com.github.damontecres.wholphin.util.UpdateChecker import com.github.damontecres.wholphin.util.profile.createDeviceProfile @@ -70,10 +71,14 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var serverEventListener: ServerEventListener + @Inject + lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver + @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) Timber.i("MainActivity.onCreate") + super.onCreate(savedInstanceState) + lifecycle.addObserver(playbackLifecycleObserver) setContent { CoilConfig(okHttpClient, false) val appPreferences by userPreferencesDataStore.data.collectAsState(null) @@ -175,11 +180,4 @@ class MainActivity : AppCompatActivity() { appUpgradeHandler.run() } } - - override fun onPause() { - if (navigationManager.backStack.lastOrNull() is Destination.Playback) { - navigationManager.goBack() - } - super.onPause() - } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index cb80363d..3cb73b14 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -17,8 +17,6 @@ import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.ExoPlayer import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackRepository @@ -33,7 +31,6 @@ import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseStream import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.MediaExtensionStatus import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.UserPreferences @@ -49,6 +46,7 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.PlayerFactory import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.TrackSupport import com.github.damontecres.wholphin.util.checkForSupport @@ -62,12 +60,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.mediaInfoApi @@ -118,29 +114,11 @@ class PlaybackViewModel val serverRepository: ServerRepository, val itemPlaybackRepository: ItemPlaybackRepository, val appPreferences: DataStore, + private val playerFactory: PlayerFactory, ) : ViewModel(), Player.Listener { val player by lazy { - val extensions = - runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled - Timber.v("extensions=$extensions") - val rendererMode = - when (extensions) { - MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON - MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER - MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF - else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON - } - ExoPlayer - .Builder(context) - .setRenderersFactory( - DefaultRenderersFactory(context) - .setEnableDecoderFallback(true) - .setExtensionRendererMode(rendererMode), - ).build() - .apply { - playWhenReady = true - } + playerFactory.createVideoPlayer() } val loading = MutableLiveData(LoadingState.Loading) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt b/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt new file mode 100644 index 00000000..fcc49917 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt @@ -0,0 +1,46 @@ +package com.github.damontecres.wholphin.util + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.nav.NavigationManager +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject + +/** + * Observes the activity lifecycle in order to pause/resume/stop playback + */ +@ActivityRetainedScoped +class PlaybackLifecycleObserver + @Inject + constructor( + private val navigationManager: NavigationManager, + private val playerFactory: PlayerFactory, + ) : DefaultLifecycleObserver { + private var wasPlaying: Boolean? = null + + override fun onStart(owner: LifecycleOwner) { + wasPlaying = null + } + + override fun onResume(owner: LifecycleOwner) { + if (wasPlaying == true) { + playerFactory.currentPlayer?.let { + if (!it.isReleased) it.play() + } + } + } + + override fun onPause(owner: LifecycleOwner) { + playerFactory.currentPlayer?.let { + wasPlaying = it.isPlaying + it.pause() + } + } + + override fun onStop(owner: LifecycleOwner) { + if (navigationManager.backStack.lastOrNull() is Destination.Playback) { + navigationManager.goBack() + } + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt new file mode 100644 index 00000000..36efeefa --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt @@ -0,0 +1,72 @@ +@file:OptIn(markerClass = [UnstableApi::class]) + +package com.github.damontecres.wholphin.util + +import android.content.Context +import androidx.annotation.OptIn +import androidx.datastore.core.DataStore +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.MediaExtensionStatus +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Constructs a [Player] instance for video playback + */ +@Singleton +class PlayerFactory + @Inject + constructor( + @param:ApplicationContext private val context: Context, + private val appPreferences: DataStore, + ) { + @Volatile + var currentPlayer: Player? = null + private set + + fun createVideoPlayer(): Player { + if (currentPlayer?.isReleased == true) { + throw IllegalStateException("Player was not released before trying to create a new one!") + } + + val extensions = + runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled + Timber.v("extensions=$extensions") + val rendererMode = + when (extensions) { + MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON + MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF + else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON + } + val newPlayer = + ExoPlayer + .Builder(context) + .setRenderersFactory( + DefaultRenderersFactory(context) + .setEnableDecoderFallback(true) + .setExtensionRendererMode(rendererMode), + ).build() + .apply { + playWhenReady = true + } + currentPlayer = newPlayer + return newPlayer + } + } + +val Player.isReleased: Boolean + get() { + return when (this) { + is ExoPlayer -> isReleased + else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}") + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt index f37dbb5f..1e50d4a0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TrackActivityPlaybackListener.kt @@ -76,13 +76,13 @@ class TrackActivityPlaybackListener( fun release() { task.cancel() TIMER.purge() - val position = player.currentPosition.milliseconds.inWholeTicks + val position = player.currentPosition.milliseconds coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) { - Timber.v("reportPlaybackStopped for ${itemPlayback.itemId}") + Timber.v("reportPlaybackStopped for ${itemPlayback.itemId} at $position") api.playStateApi.reportPlaybackStopped( PlaybackStopInfo( itemId = itemPlayback.itemId, - positionTicks = position, + positionTicks = position.inWholeTicks, failed = false, playSessionId = playback.playSessionId, liveStreamId = playback.liveStreamId,