Extract some PlayerViewModel code

This commit is contained in:
Maxr1998 committed 2023-08-23 01:32:01 +02:00
1 parent e16791a39c
commit 90bdd91f52
9 files changed
+227 -176

No files matched your search

@@ -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<PlayerEvent>() }
single(named(PLAYER_EVENT_CHANNEL)) { Channel<WebAppCommand>() }
// 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() }
@@ -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<PlayerEvent>,
private val webAppCommandChannel: Channel<WebAppCommand>,
) {
@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))
}
}
@@ -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<PlayerEvent> 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()
@@ -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,
)
}
}
}
@@ -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()
}
@@ -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()
}
@@ -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<WebAppCommand>,
) {
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()
}
}
}
}
}
}
@@ -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<PlayOptions>(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 {
@@ -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)