diff --git a/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt b/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt index a39e4691..8a2574a4 100644 --- a/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt +++ b/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt @@ -23,7 +23,9 @@ import org.jellyfin.mobile.bridge.NativePlayer import org.jellyfin.mobile.events.ActivityEventHandler import org.jellyfin.mobile.player.audio.car.LibraryBrowser import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder -import org.jellyfin.mobile.player.interaction.PlayerEvent +import org.jellyfin.mobile.player.interaction.ApiHelper +import org.jellyfin.mobile.player.interaction.WebAppCommand +import org.jellyfin.mobile.player.interaction.WebAppCommandHandler import org.jellyfin.mobile.player.qualityoptions.QualityOptionsProvider import org.jellyfin.mobile.player.source.MediaSourceResolver import org.jellyfin.mobile.player.ui.PlayerFragment @@ -52,7 +54,7 @@ val applicationModule = module { single { ImageLoader(androidApplication()) } single { PermissionRequestHelper() } single { RemoteVolumeProvider(get()) } - single(named(PLAYER_EVENT_CHANNEL)) { Channel() } + single(named(PLAYER_EVENT_CHANNEL)) { Channel() } // Controllers single { ApiClientController(get(), get(), get(), get(), get()) } @@ -60,6 +62,7 @@ val applicationModule = module { // Event handlers and channels single { ActivityEventHandler(get()) } single { WebappFunctionChannel() } + factory { WebAppCommandHandler(get(named(PLAYER_EVENT_CHANNEL))) } // Bridge interfaces single { NativePlayer(get(), get(), get(named(PLAYER_EVENT_CHANNEL))) } @@ -75,6 +78,7 @@ val applicationModule = module { single { ConnectionHelper(get(), get()) } // Media player helpers + factory { ApiHelper(get(), get()) } single { MediaSourceResolver(get()) } single { DeviceProfileBuilder(get()) } single { QualityOptionsProvider() } diff --git a/app/src/main/java/org/jellyfin/mobile/bridge/NativePlayer.kt b/app/src/main/java/org/jellyfin/mobile/bridge/NativePlayer.kt index becf4f93..467990d7 100644 --- a/app/src/main/java/org/jellyfin/mobile/bridge/NativePlayer.kt +++ b/app/src/main/java/org/jellyfin/mobile/bridge/NativePlayer.kt @@ -6,7 +6,7 @@ import org.jellyfin.mobile.app.AppPreferences import org.jellyfin.mobile.events.ActivityEvent import org.jellyfin.mobile.events.ActivityEventHandler import org.jellyfin.mobile.player.interaction.PlayOptions -import org.jellyfin.mobile.player.interaction.PlayerEvent +import org.jellyfin.mobile.player.interaction.WebAppCommand import org.jellyfin.mobile.settings.VideoPlayerType import org.jellyfin.mobile.utils.Constants import org.json.JSONObject @@ -15,7 +15,7 @@ import org.json.JSONObject class NativePlayer( private val appPreferences: AppPreferences, private val activityEventHandler: ActivityEventHandler, - private val playerEventChannel: Channel, + private val webAppCommandChannel: Channel, ) { @JavascriptInterface @@ -30,36 +30,36 @@ class NativePlayer( @JavascriptInterface fun pausePlayer() { - playerEventChannel.trySend(PlayerEvent.Pause) + webAppCommandChannel.trySend(WebAppCommand.Pause) } @JavascriptInterface fun resumePlayer() { - playerEventChannel.trySend(PlayerEvent.Resume) + webAppCommandChannel.trySend(WebAppCommand.Resume) } @JavascriptInterface fun stopPlayer() { - playerEventChannel.trySend(PlayerEvent.Stop) + webAppCommandChannel.trySend(WebAppCommand.Stop) } @JavascriptInterface fun destroyPlayer() { - playerEventChannel.trySend(PlayerEvent.Destroy) + webAppCommandChannel.trySend(WebAppCommand.Destroy) } @JavascriptInterface fun seek(ticks: Long) { - playerEventChannel.trySend(PlayerEvent.Seek(ticks / Constants.TICKS_PER_MILLISECOND)) + webAppCommandChannel.trySend(WebAppCommand.Seek(ticks / Constants.TICKS_PER_MILLISECOND)) } @JavascriptInterface fun seekMs(ms: Long) { - playerEventChannel.trySend(PlayerEvent.Seek(ms)) + webAppCommandChannel.trySend(WebAppCommand.Seek(ms)) } @JavascriptInterface fun setVolume(volume: Int) { - playerEventChannel.trySend(PlayerEvent.SetVolume(volume)) + webAppCommandChannel.trySend(WebAppCommand.SetVolume(volume)) } } diff --git a/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt b/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt index fa3bf00d..3f759d76 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package org.jellyfin.mobile.player import android.annotation.SuppressLint @@ -26,20 +28,17 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.util.Clock import com.google.android.exoplayer2.util.EventLogger import com.google.android.exoplayer2.util.MimeTypes -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.jellyfin.mobile.BuildConfig -import org.jellyfin.mobile.app.PLAYER_EVENT_CHANNEL -import org.jellyfin.mobile.player.interaction.PlayerEvent +import org.jellyfin.mobile.player.interaction.ApiHelper import org.jellyfin.mobile.player.interaction.PlayerLifecycleObserver import org.jellyfin.mobile.player.interaction.PlayerMediaSessionCallback import org.jellyfin.mobile.player.interaction.PlayerNotificationHelper +import org.jellyfin.mobile.player.interaction.WebAppCommandHandler import org.jellyfin.mobile.player.queue.QueueManager import org.jellyfin.mobile.player.source.JellyfinMediaSource import org.jellyfin.mobile.player.ui.config.DecoderType @@ -49,37 +48,25 @@ import org.jellyfin.mobile.utils.Constants import org.jellyfin.mobile.utils.Constants.SUPPORTED_VIDEO_PLAYER_PLAYBACK_ACTIONS import org.jellyfin.mobile.utils.applyDefaultAudioAttributes import org.jellyfin.mobile.utils.applyDefaultLocalAudioAttributes -import org.jellyfin.mobile.utils.extensions.getVolumeLevelPercent import org.jellyfin.mobile.utils.extensions.setVolumeLevelPercent import org.jellyfin.mobile.utils.logTracks import org.jellyfin.mobile.utils.seekToOffset import org.jellyfin.mobile.utils.setPlaybackState import org.jellyfin.mobile.utils.toMediaMetadata import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.exception.ApiClientException -import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi import org.jellyfin.sdk.api.client.extensions.hlsSegmentApi -import org.jellyfin.sdk.api.client.extensions.playStateApi -import org.jellyfin.sdk.api.operations.DisplayPreferencesApi import org.jellyfin.sdk.api.operations.HlsSegmentApi -import org.jellyfin.sdk.api.operations.PlayStateApi import org.jellyfin.sdk.model.api.PlayMethod -import org.jellyfin.sdk.model.api.PlaybackProgressInfo -import org.jellyfin.sdk.model.api.PlaybackStartInfo -import org.jellyfin.sdk.model.api.PlaybackStopInfo -import org.jellyfin.sdk.model.api.RepeatMode import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koin.core.component.inject -import org.koin.core.qualifier.named import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @Suppress("TooManyFunctions") class PlayerViewModel(application: Application) : AndroidViewModel(application), KoinComponent, Player.Listener { private val apiClient: ApiClient = get() - private val displayPreferencesApi: DisplayPreferencesApi = apiClient.displayPreferencesApi - private val playStateApi: PlayStateApi = apiClient.playStateApi + private val apiHelper: ApiHelper = get() private val hlsSegmentApi: HlsSegmentApi = apiClient.hlsSegmentApi private val lifecycleObserver = PlayerLifecycleObserver(this) @@ -116,7 +103,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), */ val playerOrNull: ExoPlayer? get() = _player.value - private val playerEventChannel: Channel by inject(named(PLAYER_EVENT_CHANNEL)) + private val webAppCommandHandler: WebAppCommandHandler by inject() val mediaSession: MediaSession by lazy { MediaSession( @@ -138,39 +125,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), // Load display preferences viewModelScope.launch { - try { - val displayPreferencesDto by displayPreferencesApi.getDisplayPreferences( - displayPreferencesId = Constants.DISPLAY_PREFERENCES_ID_USER_SETTINGS, - client = Constants.DISPLAY_PREFERENCES_CLIENT_EMBY, - ) - - val customPrefs = displayPreferencesDto.customPrefs - - displayPreferences = DisplayPreferences( - skipBackLength = customPrefs[Constants.DISPLAY_PREFERENCES_SKIP_BACK_LENGTH]?.toLongOrNull() - ?: Constants.DEFAULT_SEEK_TIME_MS, - skipForwardLength = customPrefs[Constants.DISPLAY_PREFERENCES_SKIP_FORWARD_LENGTH]?.toLongOrNull() - ?: Constants.DEFAULT_SEEK_TIME_MS, - ) - } catch (e: ApiClientException) { - Timber.e(e, "Failed to load display preferences") - } + displayPreferences = apiHelper.loadDisplayPreferences() } // Subscribe to player events from webapp - viewModelScope.launch { - for (event in playerEventChannel) { - when (event) { - PlayerEvent.Pause -> mediaSessionCallback.onPause() - PlayerEvent.Resume -> mediaSessionCallback.onPlay() - PlayerEvent.Stop, PlayerEvent.Destroy -> mediaSessionCallback.onStop() - is PlayerEvent.Seek -> playerOrNull?.seekTo(event.ms) - is PlayerEvent.SetVolume -> { - setVolume(event.volume) - playerOrNull?.reportPlaybackState() - } - } - } + with(webAppCommandHandler) { + subscribeWebAppCommands() } } @@ -255,7 +215,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), mediaSession.setMetadata(jellyfinMediaSource.toMediaMetadata()) viewModelScope.launch { - player.reportPlaybackStart(jellyfinMediaSource) + apiHelper.reportPlaybackStart(player, jellyfinMediaSource) } } @@ -263,7 +223,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), progressUpdateJob = viewModelScope.launch { while (true) { delay(Constants.PLAYER_TIME_UPDATE_RATE) - playerOrNull?.reportPlaybackState() + reportPlaybackState() } } } @@ -291,87 +251,14 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), queueManager.tryRestartPlayback() } - private suspend fun Player.reportPlaybackStart(mediaSource: JellyfinMediaSource) { - try { - playStateApi.reportPlaybackStart( - PlaybackStartInfo( - itemId = mediaSource.itemId, - playMethod = mediaSource.playMethod, - playSessionId = mediaSource.playSessionId, - audioStreamIndex = mediaSource.selectedAudioStream?.index, - subtitleStreamIndex = mediaSource.selectedSubtitleStream?.index, - isPaused = !isPlaying, - isMuted = false, - canSeek = true, - positionTicks = mediaSource.startTimeMs * Constants.TICKS_PER_MILLISECOND, - volumeLevel = audioManager.getVolumeLevelPercent(), - repeatMode = RepeatMode.REPEAT_NONE, - ), - ) - } catch (e: ApiClientException) { - Timber.e(e, "Failed to report playback start") - } - } - - private suspend fun Player.reportPlaybackState() { + suspend fun reportPlaybackState() { + val player = playerOrNull ?: return val mediaSource = mediaSourceOrNull ?: return - val playbackPositionMillis = currentPosition - if (playbackState != Player.STATE_ENDED) { - try { - playStateApi.reportPlaybackProgress( - PlaybackProgressInfo( - itemId = mediaSource.itemId, - playMethod = mediaSource.playMethod, - playSessionId = mediaSource.playSessionId, - audioStreamIndex = mediaSource.selectedAudioStream?.index, - subtitleStreamIndex = mediaSource.selectedSubtitleStream?.index, - isPaused = !isPlaying, - isMuted = false, - canSeek = true, - positionTicks = playbackPositionMillis * Constants.TICKS_PER_MILLISECOND, - volumeLevel = audioManager.getVolumeLevelPercent(), - repeatMode = RepeatMode.REPEAT_NONE, - ), - ) - } catch (e: ApiClientException) { - Timber.e(e, "Failed to report playback progress") - } - } + apiHelper.reportPlaybackState(player, mediaSource) } private fun reportPlaybackStop() { - val mediaSource = mediaSourceOrNull ?: return - val player = playerOrNull ?: return - val hasFinished = player.playbackState == Player.STATE_ENDED - val lastPositionTicks = when { - hasFinished -> mediaSource.runTimeTicks - else -> player.currentPosition * Constants.TICKS_PER_MILLISECOND - } - - // viewModelScope may already be cancelled at this point, so we need to fallback - CoroutineScope(Dispatchers.Main).launch { - try { - // Report stopped playback - playStateApi.reportPlaybackStopped( - PlaybackStopInfo( - itemId = mediaSource.itemId, - positionTicks = lastPositionTicks, - playSessionId = mediaSource.playSessionId, - failed = false, - ), - ) - - // Mark video as watched if playback finished - if (hasFinished) { - playStateApi.markPlayedItem(itemId = mediaSource.itemId) - } - - // Stop active encoding if transcoding - stopTranscoding(mediaSource) - } catch (e: ApiClientException) { - Timber.e(e, "Failed to report playback stop") - } - } + apiHelper.reportPlaybackStop(playerOrNull ?: return, mediaSourceOrNull ?: return) } suspend fun stopTranscoding(mediaSource: JellyfinMediaSource) { @@ -466,7 +353,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), releasePlayer() } - private fun setVolume(percent: Int) { + fun setVolume(percent: Int) { if (!audioManager.isVolumeFixed) audioManager.setVolumeLevelPercent(percent) } @@ -507,7 +394,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application), viewModelScope.launch { when (playbackState) { Player.STATE_READY, Player.STATE_BUFFERING -> { - player.reportPlaybackState() + reportPlaybackState() } Player.STATE_ENDED -> { reportPlaybackStop() diff --git a/app/src/main/java/org/jellyfin/mobile/player/interaction/ApiHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/interaction/ApiHelper.kt new file mode 100644 index 00000000..1e8199d6 --- /dev/null +++ b/app/src/main/java/org/jellyfin/mobile/player/interaction/ApiHelper.kt @@ -0,0 +1,143 @@ +package org.jellyfin.mobile.player.interaction + +import android.content.Context +import android.media.AudioManager +import com.google.android.exoplayer2.Player +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.jellyfin.mobile.player.source.JellyfinMediaSource +import org.jellyfin.mobile.player.ui.config.DisplayPreferences +import org.jellyfin.mobile.utils.Constants +import org.jellyfin.mobile.utils.extensions.getVolumeLevelPercent +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.exception.ApiClientException +import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi +import org.jellyfin.sdk.api.client.extensions.hlsSegmentApi +import org.jellyfin.sdk.api.client.extensions.playStateApi +import org.jellyfin.sdk.model.api.PlayMethod +import org.jellyfin.sdk.model.api.PlaybackProgressInfo +import org.jellyfin.sdk.model.api.PlaybackStartInfo +import org.jellyfin.sdk.model.api.PlaybackStopInfo +import org.jellyfin.sdk.model.api.RepeatMode +import timber.log.Timber + +class ApiHelper( + context: Context, + private val apiClient: ApiClient, +) { + private val displayPreferencesApi = apiClient.displayPreferencesApi + private val playStateApi = apiClient.playStateApi + private val hlsSegmentApi = apiClient.hlsSegmentApi + private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + suspend fun loadDisplayPreferences(): DisplayPreferences { + try { + val displayPreferencesDto by displayPreferencesApi.getDisplayPreferences( + displayPreferencesId = Constants.DISPLAY_PREFERENCES_ID_USER_SETTINGS, + client = Constants.DISPLAY_PREFERENCES_CLIENT_EMBY, + ) + + val customPrefs = displayPreferencesDto.customPrefs + + return DisplayPreferences( + skipBackLength = customPrefs[Constants.DISPLAY_PREFERENCES_SKIP_BACK_LENGTH]?.toLongOrNull() + ?: Constants.DEFAULT_SEEK_TIME_MS, + skipForwardLength = customPrefs[Constants.DISPLAY_PREFERENCES_SKIP_FORWARD_LENGTH]?.toLongOrNull() + ?: Constants.DEFAULT_SEEK_TIME_MS, + ) + } catch (e: ApiClientException) { + Timber.e(e, "Failed to load display preferences, returning defaults") + return DisplayPreferences() + } + } + + suspend fun reportPlaybackStart(player: Player, mediaSource: JellyfinMediaSource) { + try { + playStateApi.reportPlaybackStart( + PlaybackStartInfo( + itemId = mediaSource.itemId, + playMethod = mediaSource.playMethod, + playSessionId = mediaSource.playSessionId, + audioStreamIndex = mediaSource.selectedAudioStream?.index, + subtitleStreamIndex = mediaSource.selectedSubtitleStream?.index, + isPaused = !player.isPlaying, + isMuted = false, + canSeek = true, + positionTicks = mediaSource.startTimeMs * Constants.TICKS_PER_MILLISECOND, + volumeLevel = audioManager.getVolumeLevelPercent(), + repeatMode = RepeatMode.REPEAT_NONE, + ), + ) + } catch (e: ApiClientException) { + Timber.e(e, "Failed to report playback start") + } + } + + suspend fun reportPlaybackState(player: Player, mediaSource: JellyfinMediaSource) { + val playbackPositionMillis = player.currentPosition + if (player.playbackState != Player.STATE_ENDED) { + try { + playStateApi.reportPlaybackProgress( + PlaybackProgressInfo( + itemId = mediaSource.itemId, + playMethod = mediaSource.playMethod, + playSessionId = mediaSource.playSessionId, + audioStreamIndex = mediaSource.selectedAudioStream?.index, + subtitleStreamIndex = mediaSource.selectedSubtitleStream?.index, + isPaused = !player.isPlaying, + isMuted = false, + canSeek = true, + positionTicks = playbackPositionMillis * Constants.TICKS_PER_MILLISECOND, + volumeLevel = audioManager.getVolumeLevelPercent(), + repeatMode = RepeatMode.REPEAT_NONE, + ), + ) + } catch (e: ApiClientException) { + Timber.e(e, "Failed to report playback progress") + } + } + } + + fun reportPlaybackStop(player: Player, mediaSource: JellyfinMediaSource) { + val hasFinished = player.playbackState == Player.STATE_ENDED + val lastPositionTicks = when { + hasFinished -> mediaSource.runTimeTicks + else -> player.currentPosition * Constants.TICKS_PER_MILLISECOND + } + + // viewModelScope may already be cancelled at this point, so we need to fallback + CoroutineScope(Dispatchers.Main).launch { + try { + // Report stopped playback + playStateApi.reportPlaybackStopped( + PlaybackStopInfo( + itemId = mediaSource.itemId, + positionTicks = lastPositionTicks, + playSessionId = mediaSource.playSessionId, + failed = false, + ), + ) + + // Mark video as watched if playback finished + if (hasFinished) { + playStateApi.markPlayedItem(itemId = mediaSource.itemId) + } + + // Stop active encoding if transcoding + stopTranscoding(mediaSource) + } catch (e: ApiClientException) { + Timber.e(e, "Failed to report playback stop") + } + } + } + + suspend fun stopTranscoding(mediaSource: JellyfinMediaSource) { + if (mediaSource.playMethod == PlayMethod.TRANSCODE) { + hlsSegmentApi.stopEncodingProcess( + deviceId = apiClient.deviceInfo.id, + playSessionId = mediaSource.playSessionId, + ) + } + } +} diff --git a/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerEvent.kt b/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerEvent.kt deleted file mode 100644 index 3515f91f..00000000 --- a/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerEvent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.jellyfin.mobile.player.interaction - -sealed class PlayerEvent { - data object Pause : PlayerEvent() - data object Resume : PlayerEvent() - data object Stop : PlayerEvent() - data object Destroy : PlayerEvent() - data class Seek(val ms: Long) : PlayerEvent() - data class SetVolume(val volume: Int) : PlayerEvent() -} diff --git a/app/src/main/java/org/jellyfin/mobile/player/interaction/WebAppCommand.kt b/app/src/main/java/org/jellyfin/mobile/player/interaction/WebAppCommand.kt new file mode 100644 index 00000000..a225b170 --- /dev/null +++ b/app/src/main/java/org/jellyfin/mobile/player/interaction/WebAppCommand.kt @@ -0,0 +1,13 @@ +package org.jellyfin.mobile.player.interaction + +/** + * Represents a command sent to the player from the web app. + */ +sealed class WebAppCommand { + data object Pause : WebAppCommand() + data object Resume : WebAppCommand() + data object Stop : WebAppCommand() + data object Destroy : WebAppCommand() + data class Seek(val ms: Long) : WebAppCommand() + data class SetVolume(val volume: Int) : WebAppCommand() +} diff --git a/app/src/main/java/org/jellyfin/mobile/player/interaction/WebAppCommandHandler.kt b/app/src/main/java/org/jellyfin/mobile/player/interaction/WebAppCommandHandler.kt new file mode 100644 index 00000000..7709be9d --- /dev/null +++ b/app/src/main/java/org/jellyfin/mobile/player/interaction/WebAppCommandHandler.kt @@ -0,0 +1,27 @@ +package org.jellyfin.mobile.player.interaction + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.jellyfin.mobile.player.PlayerViewModel + +class WebAppCommandHandler( + private val commandChannel: Channel, +) { + fun PlayerViewModel.subscribeWebAppCommands() { + viewModelScope.launch { + for (event in commandChannel) { + when (event) { + WebAppCommand.Pause -> pause() + WebAppCommand.Resume -> play() + WebAppCommand.Stop, WebAppCommand.Destroy -> stop() + is WebAppCommand.Seek -> playerOrNull?.seekTo(event.ms) + is WebAppCommand.SetVolume -> { + setVolume(event.volume) + reportPlaybackState() + } + } + } + } + } +} diff --git a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt index 8fa207b1..485a7baf 100644 --- a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt +++ b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt @@ -95,8 +95,8 @@ class PlayerFragment : Fragment(), BackPressInterceptor { requireActivity().window.keepScreenOn = uiViewModel.shouldShowPauseButton } viewModel.error.observe(this) { message -> - val safeMessage = message.ifEmpty { requireContext().getString(R.string.player_error_unspecific_exception) } - requireContext().toast(safeMessage) + val safeMessage = message.ifEmpty { getString(R.string.player_error_unspecific_exception) } + requireActivity().toast(safeMessage) } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -119,17 +119,20 @@ class PlayerFragment : Fragment(), BackPressInterceptor { // Handle fragment arguments, extract playback options and start playback lifecycleScope.launch { - val context = requireContext() val playOptions = requireArguments().getParcelableCompat(Constants.EXTRA_MEDIA_PLAY_OPTIONS) if (playOptions == null) { - context.toast(R.string.player_error_invalid_play_options) + requireActivity().toast(R.string.player_error_invalid_play_options) return@launch } - when (viewModel.queueManager.initializePlaybackQueue(playOptions)) { - is PlayerException.InvalidPlayOptions -> context.toast(R.string.player_error_invalid_play_options) - is PlayerException.NetworkFailure -> context.toast(R.string.player_error_network_failure) - is PlayerException.UnsupportedContent -> context.toast(R.string.player_error_unsupported_content) - null -> Unit // success + + val playbackException = viewModel.queueManager.initializePlaybackQueue(playOptions) + if (playbackException != null) { + val context = requireActivity() + when (playbackException) { + is PlayerException.InvalidPlayOptions -> context.toast(R.string.player_error_invalid_play_options) + is PlayerException.NetworkFailure -> context.toast(R.string.player_error_network_failure) + is PlayerException.UnsupportedContent -> context.toast(R.string.player_error_unsupported_content) + } } } } @@ -159,15 +162,7 @@ class PlayerFragment : Fragment(), BackPressInterceptor { } ViewCompat.requestApplyInsets(view) - /*// Create playback menus - playerMenus = PlayerMenus(this, playerBinding, playerControlsBinding) - - // Set controller timeout - suppressControllerAutoHide(false)*/ - fullscreenHelper = FullscreenHelper(window) - /*playerLockScreenHelper = PlayerLockScreenHelper(this, playerBinding, orientationListener) - playerGestureHelper = PlayerGestureHelper(this, playerBinding, playerLockScreenHelper)*/ val swipeGestureHelper = SwipeGestureHelper(requireContext(), window, appPreferences) composeView.setContent { diff --git a/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt b/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt index 9829d0c5..e777418d 100644 --- a/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt +++ b/app/src/main/java/org/jellyfin/mobile/utils/Constants.kt @@ -100,19 +100,11 @@ object Constants { const val LANGUAGE_UNDEFINED = "und" const val TICKS_PER_MILLISECOND = 10000 const val PLAYER_TIME_UPDATE_RATE = 10000L - const val DEFAULT_CONTROLS_TIMEOUT_MS = 2500 - const val SWIPE_GESTURE_EXCLUSION_SIZE_VERTICAL = 64 - const val DEFAULT_CENTER_OVERLAY_TIMEOUT_MS = 250 const val DISPLAY_PREFERENCES_ID_USER_SETTINGS = "usersettings" const val DISPLAY_PREFERENCES_CLIENT_EMBY = "emby" const val DISPLAY_PREFERENCES_SKIP_BACK_LENGTH = "skipBackLength" const val DISPLAY_PREFERENCES_SKIP_FORWARD_LENGTH = "skipForwardLength" const val DEFAULT_SEEK_TIME_MS = 10000L - const val DOUBLE_TAP_RIPPLE_DURATION_MS = 100L - const val FULL_SWIPE_RANGE_SCREEN_RATIO = 0.66f - const val SCREEN_BRIGHTNESS_MAX = 255 - const val ZOOM_SCALE_BASE = 1f - const val ZOOM_SCALE_THRESHOLD = 0.01f val ASPECT_RATIO_16_9 = Rational(16, 9) val PIP_MIN_RATIONAL = Rational(100, 239) val PIP_MAX_RATIONAL = Rational(239, 100)