mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Merge branch 'main' into develop/mpv
This commit is contained in:
commit
96c856e8fc
10 changed files with 261 additions and 122 deletions
|
|
@ -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.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.util.AppUpgradeHandler
|
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.ServerEventListener
|
||||||
import com.github.damontecres.wholphin.util.UpdateChecker
|
import com.github.damontecres.wholphin.util.UpdateChecker
|
||||||
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
|
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
|
||||||
|
|
@ -70,13 +71,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serverEventListener: ServerEventListener
|
lateinit var serverEventListener: ServerEventListener
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
Timber.i("MainActivity.onCreate")
|
||||||
|
lifecycle.addObserver(playbackLifecycleObserver)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
appUpgradeHandler.copySubfont(false)
|
appUpgradeHandler.copySubfont(false)
|
||||||
}
|
}
|
||||||
Timber.i("MainActivity.onCreate")
|
|
||||||
setContent {
|
setContent {
|
||||||
CoilConfig(okHttpClient, false)
|
CoilConfig(okHttpClient, false)
|
||||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||||
|
|
@ -178,11 +183,4 @@ class MainActivity : AppCompatActivity() {
|
||||||
appUpgradeHandler.run()
|
appUpgradeHandler.run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
if (navigationManager.backStack.lastOrNull() is Destination.Playback) {
|
|
||||||
navigationManager.goBack()
|
|
||||||
}
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ fun RecommendedMovie(
|
||||||
viewModel.init()
|
viewModel.init()
|
||||||
}
|
}
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val rows by viewModel.rows.collectAsState(listOf())
|
val rows by viewModel.rows.collectAsState()
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ fun RecommendedTvShow(
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val rows by viewModel.rows.collectAsState(listOf())
|
val rows by viewModel.rows.collectAsState()
|
||||||
|
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,9 @@ fun HomePage(
|
||||||
firstLoad = false
|
firstLoad = false
|
||||||
}
|
}
|
||||||
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
||||||
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
val refreshing by viewModel.refreshState.observeAsState(LoadingState.Loading)
|
||||||
|
val watchingRows by viewModel.watchingRows.observeAsState(listOf())
|
||||||
|
val latestRows by viewModel.latestRows.observeAsState(listOf())
|
||||||
LaunchedEffect(loading) {
|
LaunchedEffect(loading) {
|
||||||
val state = loading
|
val state = loading
|
||||||
if (!firstLoad && state is LoadingState.Error) {
|
if (!firstLoad && state is LoadingState.Error) {
|
||||||
|
|
@ -106,7 +108,7 @@ fun HomePage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val state = if (firstLoad) loading else LoadingState.Success) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
||||||
LoadingState.Loading,
|
LoadingState.Loading,
|
||||||
|
|
@ -118,7 +120,7 @@ fun HomePage(
|
||||||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
HomePageContent(
|
HomePageContent(
|
||||||
homeRows,
|
watchingRows + latestRows,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
viewModel.navigationManager.navigateTo(it.destination())
|
viewModel.navigationManager.navigateTo(it.destination())
|
||||||
},
|
},
|
||||||
|
|
@ -154,7 +156,7 @@ fun HomePage(
|
||||||
items = dialogItems,
|
items = dialogItems,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
loadingState = loading,
|
loadingState = refreshing,
|
||||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
@ -277,7 +279,7 @@ fun HomePageContent(
|
||||||
-> {
|
-> {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = Modifier.animateItem(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = r.title,
|
text = r.title,
|
||||||
|
|
@ -295,7 +297,7 @@ fun HomePageContent(
|
||||||
is HomeRowLoadingState.Error -> {
|
is HomeRowLoadingState.Error -> {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = Modifier.animateItem(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = r.title,
|
text = r.title,
|
||||||
|
|
@ -323,7 +325,10 @@ fun HomePageContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateItem(),
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
// TODO better aspect ration handling?
|
// TODO better aspect ration handling?
|
||||||
BannerCard(
|
BannerCard(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||||
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
|
@ -51,12 +52,18 @@ class HomeViewModel
|
||||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val homeRows = MutableLiveData<List<HomeRowLoadingState>>()
|
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
val watchingRows = MutableLiveData<List<HomeRowLoadingState>>(listOf())
|
||||||
|
val latestRows = MutableLiveData<List<HomeRowLoadingState>>(listOf())
|
||||||
|
|
||||||
private lateinit var preferences: UserPreferences
|
private lateinit var preferences: UserPreferences
|
||||||
|
|
||||||
fun init(preferences: UserPreferences): Job {
|
fun init(preferences: UserPreferences): Job {
|
||||||
|
val reload = loadingState.value != LoadingState.Success
|
||||||
|
if (reload) {
|
||||||
loadingState.value = LoadingState.Loading
|
loadingState.value = LoadingState.Loading
|
||||||
|
}
|
||||||
|
refreshState.value = LoadingState.Loading
|
||||||
this.preferences = preferences
|
this.preferences = preferences
|
||||||
val prefs = preferences.appPreferences.homePagePreferences
|
val prefs = preferences.appPreferences.homePagePreferences
|
||||||
val limit = prefs.maxItemsPerRow
|
val limit = prefs.maxItemsPerRow
|
||||||
|
|
@ -84,9 +91,7 @@ class HomeViewModel
|
||||||
prefs.enableRewatchingNextUp,
|
prefs.enableRewatchingNextUp,
|
||||||
prefs.combineContinueNext,
|
prefs.combineContinueNext,
|
||||||
)
|
)
|
||||||
val latest = getLatest(userDto, limit, includedIds)
|
val watching =
|
||||||
|
|
||||||
val homeRows =
|
|
||||||
buildList {
|
buildList {
|
||||||
if (resume.isNotEmpty()) {
|
if (resume.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
|
|
@ -104,12 +109,20 @@ class HomeViewModel
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addAll(latest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val latest = getLatest(userDto, limit, includedIds)
|
||||||
|
val pendingLatest = latest.map { HomeRowLoadingState.Loading(it.title) }
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@HomeViewModel.homeRows.value = homeRows
|
this@HomeViewModel.watchingRows.value = watching
|
||||||
|
if (reload) {
|
||||||
|
this@HomeViewModel.latestRows.value = pendingLatest
|
||||||
|
}
|
||||||
loadingState.value = LoadingState.Success
|
loadingState.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
|
loadLatest(latest)
|
||||||
|
refreshState.setValueOnMain(LoadingState.Success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +153,7 @@ class HomeViewModel
|
||||||
.getResumeItems(request)
|
.getResumeItems(request)
|
||||||
.content
|
.content
|
||||||
.items
|
.items
|
||||||
.map { BaseItem.Companion.from(it, api, true) }
|
.map { BaseItem.from(it, api, true) }
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,29 +187,21 @@ class HomeViewModel
|
||||||
user: UserDto,
|
user: UserDto,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
includedIds: List<UUID>,
|
includedIds: List<UUID>,
|
||||||
): List<HomeRowLoadingState> {
|
): List<LatestData> {
|
||||||
val latestMediaIncludes =
|
val excluded = user.configuration?.latestItemsExcludes.orEmpty()
|
||||||
user.configuration
|
|
||||||
?.orderedViews
|
|
||||||
.orEmpty()
|
|
||||||
.toMutableList()
|
|
||||||
.apply {
|
|
||||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
|
||||||
}.filter { includedIds.contains(it) }
|
|
||||||
|
|
||||||
val views by api.userViewsApi.getUserViews()
|
val views by api.userViewsApi.getUserViews()
|
||||||
val rows =
|
val latestData =
|
||||||
latestMediaIncludes
|
views.items
|
||||||
.mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } }
|
.filter {
|
||||||
.filter { it.collectionType in supportedLatestCollectionTypes }
|
it.id in includedIds && it.id !in excluded &&
|
||||||
.mapNotNull { view ->
|
it.collectionType in supportedLatestCollectionTypes
|
||||||
|
}.mapNotNull { view ->
|
||||||
val title =
|
val title =
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
context.getString(R.string.recently_recorded)
|
context.getString(R.string.recently_recorded)
|
||||||
} else {
|
} else {
|
||||||
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||||
} ?: context.getString(R.string.recently_added)
|
} ?: context.getString(R.string.recently_added)
|
||||||
try {
|
|
||||||
val viewId =
|
val viewId =
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
|
|
@ -218,6 +223,17 @@ class HomeViewModel
|
||||||
limit = limit,
|
limit = limit,
|
||||||
isPlayed = null, // Server will handle user's preference
|
isPlayed = null, // Server will handle user's preference
|
||||||
)
|
)
|
||||||
|
LatestData(title, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestData
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadLatest(latestData: List<LatestData>) {
|
||||||
|
val rows =
|
||||||
|
latestData.map { (title, request) ->
|
||||||
|
try {
|
||||||
val latest =
|
val latest =
|
||||||
api.userLibraryApi
|
api.userLibraryApi
|
||||||
.getLatestMedia(request)
|
.getLatestMedia(request)
|
||||||
|
|
@ -227,7 +243,6 @@ class HomeViewModel
|
||||||
title = title,
|
title = title,
|
||||||
items = latest,
|
items = latest,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Exception fetching %s", title)
|
Timber.e(ex, "Exception fetching %s", title)
|
||||||
HomeRowLoadingState.Error(
|
HomeRowLoadingState.Error(
|
||||||
|
|
@ -236,8 +251,7 @@ class HomeViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
latestRows.setValueOnMain(rows)
|
||||||
return rows
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWatched(
|
fun setWatched(
|
||||||
|
|
@ -275,3 +289,8 @@ val supportedLatestCollectionTypes =
|
||||||
CollectionType.TVSHOWS,
|
CollectionType.TVSHOWS,
|
||||||
CollectionType.LIVETV,
|
CollectionType.LIVETV,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class LatestData(
|
||||||
|
val title: String,
|
||||||
|
val request: GetLatestMediaRequest,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,20 @@ fun DestinationContent(
|
||||||
|
|
||||||
BaseItemKind.USER_VIEW ->
|
BaseItemKind.USER_VIEW ->
|
||||||
when (destination.item?.data?.collectionType) {
|
when (destination.item?.data?.collectionType) {
|
||||||
|
CollectionType.TVSHOWS ->
|
||||||
|
CollectionFolderTv(
|
||||||
|
preferences,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
CollectionType.MOVIES ->
|
||||||
|
CollectionFolderMovie(
|
||||||
|
preferences,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
CollectionType.LIVETV ->
|
CollectionType.LIVETV ->
|
||||||
CollectionFolderLiveTv(
|
CollectionFolderLiveTv(
|
||||||
preferences,
|
preferences,
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ import androidx.media3.common.Player
|
||||||
import androidx.media3.common.TrackSelectionOverride
|
import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
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.data.model.chooseStream
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
||||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||||
|
|
@ -50,11 +47,11 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
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.TrackActivityPlaybackListener
|
||||||
import com.github.damontecres.wholphin.util.TrackSupport
|
import com.github.damontecres.wholphin.util.TrackSupport
|
||||||
import com.github.damontecres.wholphin.util.checkForSupport
|
import com.github.damontecres.wholphin.util.checkForSupport
|
||||||
import com.github.damontecres.wholphin.util.formatDateTime
|
import com.github.damontecres.wholphin.util.formatDateTime
|
||||||
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
|
||||||
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
|
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
|
||||||
import com.github.damontecres.wholphin.util.seasonEpisodePadded
|
import com.github.damontecres.wholphin.util.seasonEpisodePadded
|
||||||
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
||||||
|
|
@ -65,12 +62,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||||
|
|
@ -123,44 +118,11 @@ class PlaybackViewModel
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
val appPreferences: DataStore<AppPreferences>,
|
val appPreferences: DataStore<AppPreferences>,
|
||||||
|
private val playerFactory: PlayerFactory,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
Player.Listener {
|
Player.Listener {
|
||||||
val player by lazy {
|
val player by lazy {
|
||||||
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
|
playerFactory.createVideoPlayer()
|
||||||
|
|
||||||
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
|
|
||||||
when (backend) {
|
|
||||||
PlayerBackend.MPV -> {
|
|
||||||
val enableHardwareDecoding =
|
|
||||||
prefs?.mpvOptions?.enableHardwareDecoding
|
|
||||||
?: AppPreference.MpvHardwareDecoding.defaultValue
|
|
||||||
MpvPlayer(context, enableHardwareDecoding)
|
|
||||||
.apply {
|
|
||||||
playWhenReady = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerBackend.EXO_PLAYER,
|
|
||||||
PlayerBackend.UNRECOGNIZED,
|
|
||||||
-> {
|
|
||||||
val extensions = prefs?.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
@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.AppPreference
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
||||||
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||||
|
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<AppPreferences>,
|
||||||
|
) {
|
||||||
|
@Volatile
|
||||||
|
var currentPlayer: Player? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun createVideoPlayer(): Player {
|
||||||
|
if (currentPlayer?.isReleased == false) {
|
||||||
|
Timber.w("Player was not released before trying to create a new one!")
|
||||||
|
currentPlayer?.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
|
||||||
|
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
|
||||||
|
val newPlayer =
|
||||||
|
when (backend) {
|
||||||
|
PlayerBackend.MPV -> {
|
||||||
|
val enableHardwareDecoding =
|
||||||
|
prefs?.mpvOptions?.enableHardwareDecoding
|
||||||
|
?: AppPreference.MpvHardwareDecoding.defaultValue
|
||||||
|
MpvPlayer(context, enableHardwareDecoding)
|
||||||
|
.apply {
|
||||||
|
playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
PlayerBackend.UNRECOGNIZED,
|
||||||
|
-> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPlayer = newPlayer
|
||||||
|
return newPlayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Player.isReleased: Boolean
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
is ExoPlayer -> isReleased
|
||||||
|
is MpvPlayer -> isReleased
|
||||||
|
else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -76,13 +76,13 @@ class TrackActivityPlaybackListener(
|
||||||
fun release() {
|
fun release() {
|
||||||
task.cancel()
|
task.cancel()
|
||||||
TIMER.purge()
|
TIMER.purge()
|
||||||
val position = player.currentPosition.milliseconds.inWholeTicks
|
val position = player.currentPosition.milliseconds
|
||||||
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
Timber.v("reportPlaybackStopped for ${itemPlayback.itemId}")
|
Timber.v("reportPlaybackStopped for ${itemPlayback.itemId} at $position")
|
||||||
api.playStateApi.reportPlaybackStopped(
|
api.playStateApi.reportPlaybackStopped(
|
||||||
PlaybackStopInfo(
|
PlaybackStopInfo(
|
||||||
itemId = itemPlayback.itemId,
|
itemId = itemPlayback.itemId,
|
||||||
positionTicks = position,
|
positionTicks = position.inWholeTicks,
|
||||||
failed = false,
|
failed = false,
|
||||||
playSessionId = playback.playSessionId,
|
playSessionId = playback.playSessionId,
|
||||||
liveStreamId = playback.liveStreamId,
|
liveStreamId = playback.liveStreamId,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue